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

42b0161d1569bdc5f6174d6ffe71be1b015df1ed
[etc/vim.git] / tests / test_black.py
1 #!/usr/bin/env python3
2
3 import asyncio
4 import inspect
5 import io
6 import logging
7 import multiprocessing
8 import os
9 import re
10 import sys
11 import types
12 import unittest
13 from concurrent.futures import ThreadPoolExecutor
14 from contextlib import contextmanager, redirect_stderr
15 from dataclasses import replace
16 from io import BytesIO
17 from pathlib import Path
18 from platform import system
19 from tempfile import TemporaryDirectory
20 from typing import (
21     Any,
22     Callable,
23     Dict,
24     Iterator,
25     List,
26     Optional,
27     Sequence,
28     Type,
29     TypeVar,
30     Union,
31 )
32 from unittest.mock import MagicMock, patch
33
34 import click
35 import pytest
36 from click import unstyle
37 from click.testing import CliRunner
38 from pathspec import PathSpec
39
40 import black
41 import black.files
42 from black import Feature, TargetVersion
43 from black import re_compile_maybe_verbose as compile_pattern
44 from black.cache import get_cache_dir, get_cache_file
45 from black.debug import DebugVisitor
46 from black.output import color_diff, diff
47 from black.report import Report
48
49 # Import other test classes
50 from tests.util import (
51     DATA_DIR,
52     DEFAULT_MODE,
53     DETERMINISTIC_HEADER,
54     PROJECT_ROOT,
55     PY36_VERSIONS,
56     THIS_DIR,
57     BlackBaseTestCase,
58     assert_format,
59     change_directory,
60     dump_to_stderr,
61     ff,
62     fs,
63     get_case_path,
64     read_data,
65     read_data_from_file,
66 )
67
68 THIS_FILE = Path(__file__)
69 EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
70 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
71 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
72 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
73 T = TypeVar("T")
74 R = TypeVar("R")
75
76 # Match the time output in a diff, but nothing else
77 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
78
79
80 @contextmanager
81 def cache_dir(exists: bool = True) -> Iterator[Path]:
82     with TemporaryDirectory() as workspace:
83         cache_dir = Path(workspace)
84         if not exists:
85             cache_dir = cache_dir / "new"
86         with patch("black.cache.CACHE_DIR", cache_dir):
87             yield cache_dir
88
89
90 @contextmanager
91 def event_loop() -> Iterator[None]:
92     policy = asyncio.get_event_loop_policy()
93     loop = policy.new_event_loop()
94     asyncio.set_event_loop(loop)
95     try:
96         yield
97
98     finally:
99         loop.close()
100
101
102 class FakeContext(click.Context):
103     """A fake click Context for when calling functions that need it."""
104
105     def __init__(self) -> None:
106         self.default_map: Dict[str, Any] = {}
107         # Dummy root, since most of the tests don't care about it
108         self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
109
110
111 class FakeParameter(click.Parameter):
112     """A fake click Parameter for when calling functions that need it."""
113
114     def __init__(self) -> None:
115         pass
116
117
118 class BlackRunner(CliRunner):
119     """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
120
121     def __init__(self) -> None:
122         super().__init__(mix_stderr=False)
123
124
125 def invokeBlack(
126     args: List[str], exit_code: int = 0, ignore_config: bool = True
127 ) -> None:
128     runner = BlackRunner()
129     if ignore_config:
130         args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
131     result = runner.invoke(black.main, args, catch_exceptions=False)
132     assert result.stdout_bytes is not None
133     assert result.stderr_bytes is not None
134     msg = (
135         f"Failed with args: {args}\n"
136         f"stdout: {result.stdout_bytes.decode()!r}\n"
137         f"stderr: {result.stderr_bytes.decode()!r}\n"
138         f"exception: {result.exception}"
139     )
140     assert result.exit_code == exit_code, msg
141
142
143 class BlackTestCase(BlackBaseTestCase):
144     invokeBlack = staticmethod(invokeBlack)
145
146     def test_empty_ff(self) -> None:
147         expected = ""
148         tmp_file = Path(black.dump_to_file())
149         try:
150             self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
151             with open(tmp_file, encoding="utf8") as f:
152                 actual = f.read()
153         finally:
154             os.unlink(tmp_file)
155         self.assertFormatEqual(expected, actual)
156
157     @patch("black.dump_to_file", dump_to_stderr)
158     def test_one_empty_line(self) -> None:
159         mode = black.Mode(preview=True)
160         for nl in ["\n", "\r\n"]:
161             source = expected = nl
162             assert_format(source, expected, mode=mode)
163
164     def test_one_empty_line_ff(self) -> None:
165         mode = black.Mode(preview=True)
166         for nl in ["\n", "\r\n"]:
167             expected = nl
168             tmp_file = Path(black.dump_to_file(nl))
169             if system() == "Windows":
170                 # Writing files in text mode automatically uses the system newline,
171                 # but in this case we don't want this for testing reasons. See:
172                 # https://github.com/psf/black/pull/3348
173                 with open(tmp_file, "wb") as f:
174                     f.write(nl.encode("utf-8"))
175             try:
176                 self.assertFalse(
177                     ff(tmp_file, mode=mode, write_back=black.WriteBack.YES)
178                 )
179                 with open(tmp_file, "rb") as f:
180                     actual = f.read().decode("utf8")
181             finally:
182                 os.unlink(tmp_file)
183             self.assertFormatEqual(expected, actual)
184
185     def test_experimental_string_processing_warns(self) -> None:
186         self.assertWarns(
187             black.mode.Deprecated, black.Mode, experimental_string_processing=True
188         )
189
190     def test_piping(self) -> None:
191         source, expected = read_data_from_file(PROJECT_ROOT / "src/black/__init__.py")
192         result = BlackRunner().invoke(
193             black.main,
194             [
195                 "-",
196                 "--fast",
197                 f"--line-length={black.DEFAULT_LINE_LENGTH}",
198                 f"--config={EMPTY_CONFIG}",
199             ],
200             input=BytesIO(source.encode("utf8")),
201         )
202         self.assertEqual(result.exit_code, 0)
203         self.assertFormatEqual(expected, result.output)
204         if source != result.output:
205             black.assert_equivalent(source, result.output)
206             black.assert_stable(source, result.output, DEFAULT_MODE)
207
208     def test_piping_diff(self) -> None:
209         diff_header = re.compile(
210             r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
211             r"\+\d\d\d\d"
212         )
213         source, _ = read_data("simple_cases", "expression.py")
214         expected, _ = read_data("simple_cases", "expression.diff")
215         args = [
216             "-",
217             "--fast",
218             f"--line-length={black.DEFAULT_LINE_LENGTH}",
219             "--diff",
220             f"--config={EMPTY_CONFIG}",
221         ]
222         result = BlackRunner().invoke(
223             black.main, args, input=BytesIO(source.encode("utf8"))
224         )
225         self.assertEqual(result.exit_code, 0)
226         actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
227         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
228         self.assertEqual(expected, actual)
229
230     def test_piping_diff_with_color(self) -> None:
231         source, _ = read_data("simple_cases", "expression.py")
232         args = [
233             "-",
234             "--fast",
235             f"--line-length={black.DEFAULT_LINE_LENGTH}",
236             "--diff",
237             "--color",
238             f"--config={EMPTY_CONFIG}",
239         ]
240         result = BlackRunner().invoke(
241             black.main, args, input=BytesIO(source.encode("utf8"))
242         )
243         actual = result.output
244         # Again, the contents are checked in a different test, so only look for colors.
245         self.assertIn("\033[1m", actual)
246         self.assertIn("\033[36m", actual)
247         self.assertIn("\033[32m", actual)
248         self.assertIn("\033[31m", actual)
249         self.assertIn("\033[0m", actual)
250
251     @patch("black.dump_to_file", dump_to_stderr)
252     def _test_wip(self) -> None:
253         source, expected = read_data("miscellaneous", "wip")
254         sys.settrace(tracefunc)
255         mode = replace(
256             DEFAULT_MODE,
257             experimental_string_processing=False,
258             target_versions={black.TargetVersion.PY38},
259         )
260         actual = fs(source, mode=mode)
261         sys.settrace(None)
262         self.assertFormatEqual(expected, actual)
263         black.assert_equivalent(source, actual)
264         black.assert_stable(source, actual, black.FileMode())
265
266     def test_pep_572_version_detection(self) -> None:
267         source, _ = read_data("py_38", "pep_572")
268         root = black.lib2to3_parse(source)
269         features = black.get_features_used(root)
270         self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
271         versions = black.detect_target_versions(root)
272         self.assertIn(black.TargetVersion.PY38, versions)
273
274     def test_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             with open(tmp_file, encoding="utf8") as f:
289                 actual = f.read()
290         finally:
291             os.unlink(tmp_file)
292         self.assertFormatEqual(expected, actual)
293         with patch("black.dump_to_file", dump_to_stderr):
294             black.assert_equivalent(source, actual)
295             black.assert_stable(source, actual, DEFAULT_MODE)
296
297     def test_expression_diff(self) -> None:
298         source, _ = read_data("simple_cases", "expression.py")
299         expected, _ = read_data("simple_cases", "expression.diff")
300         tmp_file = Path(black.dump_to_file(source))
301         diff_header = re.compile(
302             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
303             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
304         )
305         try:
306             result = BlackRunner().invoke(
307                 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
308             )
309             self.assertEqual(result.exit_code, 0)
310         finally:
311             os.unlink(tmp_file)
312         actual = result.output
313         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
314         if expected != actual:
315             dump = black.dump_to_file(actual)
316             msg = (
317                 "Expected diff isn't equal to the actual. If you made changes to"
318                 " expression.py and this is an anticipated difference, overwrite"
319                 f" tests/data/expression.diff with {dump}"
320             )
321             self.assertEqual(expected, actual, msg)
322
323     def test_expression_diff_with_color(self) -> None:
324         source, _ = read_data("simple_cases", "expression.py")
325         expected, _ = read_data("simple_cases", "expression.diff")
326         tmp_file = Path(black.dump_to_file(source))
327         try:
328             result = BlackRunner().invoke(
329                 black.main,
330                 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
331             )
332         finally:
333             os.unlink(tmp_file)
334         actual = result.output
335         # We check the contents of the diff in `test_expression_diff`. All
336         # we need to check here is that color codes exist in the result.
337         self.assertIn("\033[1m", actual)
338         self.assertIn("\033[36m", actual)
339         self.assertIn("\033[32m", actual)
340         self.assertIn("\033[31m", actual)
341         self.assertIn("\033[0m", actual)
342
343     def test_detect_pos_only_arguments(self) -> None:
344         source, _ = read_data("py_38", "pep_570")
345         root = black.lib2to3_parse(source)
346         features = black.get_features_used(root)
347         self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
348         versions = black.detect_target_versions(root)
349         self.assertIn(black.TargetVersion.PY38, versions)
350
351     def test_detect_debug_f_strings(self) -> None:
352         root = black.lib2to3_parse("""f"{x=}" """)
353         features = black.get_features_used(root)
354         self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
355         versions = black.detect_target_versions(root)
356         self.assertIn(black.TargetVersion.PY38, versions)
357
358         root = black.lib2to3_parse(
359             """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
360         )
361         features = black.get_features_used(root)
362         self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
363
364         # We don't yet support feature version detection in nested f-strings
365         root = black.lib2to3_parse(
366             """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
367         )
368         features = black.get_features_used(root)
369         self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
370
371     @patch("black.dump_to_file", dump_to_stderr)
372     def test_string_quotes(self) -> None:
373         source, expected = read_data("miscellaneous", "string_quotes")
374         mode = black.Mode(preview=True)
375         assert_format(source, expected, mode)
376         mode = replace(mode, string_normalization=False)
377         not_normalized = fs(source, mode=mode)
378         self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
379         black.assert_equivalent(source, not_normalized)
380         black.assert_stable(source, not_normalized, mode=mode)
381
382     def test_skip_source_first_line(self) -> None:
383         source, _ = read_data("miscellaneous", "invalid_header")
384         tmp_file = Path(black.dump_to_file(source))
385         # Full source should fail (invalid syntax at header)
386         self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
387         # So, skipping the first line should work
388         result = BlackRunner().invoke(
389             black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
390         )
391         self.assertEqual(result.exit_code, 0)
392         with open(tmp_file, encoding="utf8") as f:
393             actual = f.read()
394         self.assertFormatEqual(source, actual)
395
396     def test_skip_source_first_line_when_mixing_newlines(self) -> None:
397         code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
398         expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
399         with TemporaryDirectory() as workspace:
400             test_file = Path(workspace) / "skip_header.py"
401             test_file.write_bytes(code_mixing_newlines)
402             mode = replace(DEFAULT_MODE, skip_source_first_line=True)
403             ff(test_file, mode=mode, write_back=black.WriteBack.YES)
404             self.assertEqual(test_file.read_bytes(), expected)
405
406     def test_skip_magic_trailing_comma(self) -> None:
407         source, _ = read_data("simple_cases", "expression")
408         expected, _ = read_data(
409             "miscellaneous", "expression_skip_magic_trailing_comma.diff"
410         )
411         tmp_file = Path(black.dump_to_file(source))
412         diff_header = re.compile(
413             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
414             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
415         )
416         try:
417             result = BlackRunner().invoke(
418                 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
419             )
420             self.assertEqual(result.exit_code, 0)
421         finally:
422             os.unlink(tmp_file)
423         actual = result.output
424         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
425         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
426         if expected != actual:
427             dump = black.dump_to_file(actual)
428             msg = (
429                 "Expected diff isn't equal to the actual. If you made changes to"
430                 " expression.py and this is an anticipated difference, overwrite"
431                 " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
432                 f" with {dump}"
433             )
434             self.assertEqual(expected, actual, msg)
435
436     @patch("black.dump_to_file", dump_to_stderr)
437     def test_async_as_identifier(self) -> None:
438         source_path = get_case_path("miscellaneous", "async_as_identifier")
439         source, expected = read_data_from_file(source_path)
440         actual = fs(source)
441         self.assertFormatEqual(expected, actual)
442         major, minor = sys.version_info[:2]
443         if major < 3 or (major <= 3 and minor < 7):
444             black.assert_equivalent(source, actual)
445         black.assert_stable(source, actual, DEFAULT_MODE)
446         # ensure black can parse this when the target is 3.6
447         self.invokeBlack([str(source_path), "--target-version", "py36"])
448         # but not on 3.7, because async/await is no longer an identifier
449         self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
450
451     @patch("black.dump_to_file", dump_to_stderr)
452     def test_python37(self) -> None:
453         source_path = get_case_path("py_37", "python37")
454         source, expected = read_data_from_file(source_path)
455         actual = fs(source)
456         self.assertFormatEqual(expected, actual)
457         major, minor = sys.version_info[:2]
458         if major > 3 or (major == 3 and minor >= 7):
459             black.assert_equivalent(source, actual)
460         black.assert_stable(source, actual, DEFAULT_MODE)
461         # ensure black can parse this when the target is 3.7
462         self.invokeBlack([str(source_path), "--target-version", "py37"])
463         # but not on 3.6, because we use async as a reserved keyword
464         self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
465
466     def test_tab_comment_indentation(self) -> None:
467         contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
468         contents_spc = "if 1:\n    if 2:\n        pass\n    # comment\n    pass\n"
469         self.assertFormatEqual(contents_spc, fs(contents_spc))
470         self.assertFormatEqual(contents_spc, fs(contents_tab))
471
472         contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
473         contents_spc = "if 1:\n    if 2:\n        pass\n        # comment\n    pass\n"
474         self.assertFormatEqual(contents_spc, fs(contents_spc))
475         self.assertFormatEqual(contents_spc, fs(contents_tab))
476
477         # mixed tabs and spaces (valid Python 2 code)
478         contents_tab = "if 1:\n        if 2:\n\t\tpass\n\t# comment\n        pass\n"
479         contents_spc = "if 1:\n    if 2:\n        pass\n    # comment\n    pass\n"
480         self.assertFormatEqual(contents_spc, fs(contents_spc))
481         self.assertFormatEqual(contents_spc, fs(contents_tab))
482
483         contents_tab = "if 1:\n        if 2:\n\t\tpass\n\t\t# comment\n        pass\n"
484         contents_spc = "if 1:\n    if 2:\n        pass\n        # comment\n    pass\n"
485         self.assertFormatEqual(contents_spc, fs(contents_spc))
486         self.assertFormatEqual(contents_spc, fs(contents_tab))
487
488     def test_false_positive_symlink_output_issue_3384(self) -> None:
489         # Emulate the behavior when using the CLI (`black ./child  --verbose`), which
490         # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
491         # patched only on its first call: when checking if "./child" is a directory it
492         # should return True. The "./child" folder exists relative to the cwd when
493         # running from CLI, but fails when running the tests because cwd is different
494         project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
495         working_directory = project_root / "root"
496         target_abspath = working_directory / "child"
497         target_contents = (
498             src.relative_to(working_directory) for src in target_abspath.iterdir()
499         )
500
501         def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
502             def _mocked_calls() -> bool:
503                 if responses:
504                     return responses.pop(0)
505                 return False
506
507             return _mocked_calls
508
509         with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
510             "pathlib.Path.cwd", return_value=working_directory
511         ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
512             ctx = FakeContext()
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("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')
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             with open(path, "w") as fh:
1121                 fh.write(contents)
1122             self.invokeBlack([str(path), "--pyi"])
1123             with open(path, "r") as fh:
1124                 actual = fh.read()
1125             # verify cache with --pyi is separate
1126             pyi_cache = black.read_cache(pyi_mode)
1127             self.assertIn(str(path), pyi_cache)
1128             normal_cache = black.read_cache(DEFAULT_MODE)
1129             self.assertNotIn(str(path), normal_cache)
1130         self.assertFormatEqual(expected, actual)
1131         black.assert_equivalent(contents, actual)
1132         black.assert_stable(contents, actual, pyi_mode)
1133
1134     @event_loop()
1135     def test_multi_file_force_pyi(self) -> None:
1136         reg_mode = DEFAULT_MODE
1137         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1138         contents, expected = read_data("miscellaneous", "force_pyi")
1139         with cache_dir() as workspace:
1140             paths = [
1141                 (workspace / "file1.py").resolve(),
1142                 (workspace / "file2.py").resolve(),
1143             ]
1144             for path in paths:
1145                 with open(path, "w") as fh:
1146                     fh.write(contents)
1147             self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1148             for path in paths:
1149                 with open(path, "r") as fh:
1150                     actual = fh.read()
1151                 self.assertEqual(actual, expected)
1152             # verify cache with --pyi is separate
1153             pyi_cache = black.read_cache(pyi_mode)
1154             normal_cache = black.read_cache(reg_mode)
1155             for path in paths:
1156                 self.assertIn(str(path), pyi_cache)
1157                 self.assertNotIn(str(path), normal_cache)
1158
1159     def test_pipe_force_pyi(self) -> None:
1160         source, expected = read_data("miscellaneous", "force_pyi")
1161         result = CliRunner().invoke(
1162             black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1163         )
1164         self.assertEqual(result.exit_code, 0)
1165         actual = result.output
1166         self.assertFormatEqual(actual, expected)
1167
1168     def test_single_file_force_py36(self) -> None:
1169         reg_mode = DEFAULT_MODE
1170         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1171         source, expected = read_data("miscellaneous", "force_py36")
1172         with cache_dir() as workspace:
1173             path = (workspace / "file.py").resolve()
1174             with open(path, "w") as fh:
1175                 fh.write(source)
1176             self.invokeBlack([str(path), *PY36_ARGS])
1177             with open(path, "r") as fh:
1178                 actual = fh.read()
1179             # verify cache with --target-version is separate
1180             py36_cache = black.read_cache(py36_mode)
1181             self.assertIn(str(path), py36_cache)
1182             normal_cache = black.read_cache(reg_mode)
1183             self.assertNotIn(str(path), normal_cache)
1184         self.assertEqual(actual, expected)
1185
1186     @event_loop()
1187     def test_multi_file_force_py36(self) -> None:
1188         reg_mode = DEFAULT_MODE
1189         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1190         source, expected = read_data("miscellaneous", "force_py36")
1191         with cache_dir() as workspace:
1192             paths = [
1193                 (workspace / "file1.py").resolve(),
1194                 (workspace / "file2.py").resolve(),
1195             ]
1196             for path in paths:
1197                 with open(path, "w") as fh:
1198                     fh.write(source)
1199             self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1200             for path in paths:
1201                 with open(path, "r") as fh:
1202                     actual = fh.read()
1203                 self.assertEqual(actual, expected)
1204             # verify cache with --target-version is separate
1205             pyi_cache = black.read_cache(py36_mode)
1206             normal_cache = black.read_cache(reg_mode)
1207             for path in paths:
1208                 self.assertIn(str(path), pyi_cache)
1209                 self.assertNotIn(str(path), normal_cache)
1210
1211     def test_pipe_force_py36(self) -> None:
1212         source, expected = read_data("miscellaneous", "force_py36")
1213         result = CliRunner().invoke(
1214             black.main,
1215             ["-", "-q", "--target-version=py36"],
1216             input=BytesIO(source.encode("utf8")),
1217         )
1218         self.assertEqual(result.exit_code, 0)
1219         actual = result.output
1220         self.assertFormatEqual(actual, expected)
1221
1222     @pytest.mark.incompatible_with_mypyc
1223     def test_reformat_one_with_stdin(self) -> None:
1224         with patch(
1225             "black.format_stdin_to_stdout",
1226             return_value=lambda *args, **kwargs: black.Changed.YES,
1227         ) as fsts:
1228             report = MagicMock()
1229             path = Path("-")
1230             black.reformat_one(
1231                 path,
1232                 fast=True,
1233                 write_back=black.WriteBack.YES,
1234                 mode=DEFAULT_MODE,
1235                 report=report,
1236             )
1237             fsts.assert_called_once()
1238             report.done.assert_called_with(path, black.Changed.YES)
1239
1240     @pytest.mark.incompatible_with_mypyc
1241     def test_reformat_one_with_stdin_filename(self) -> None:
1242         with patch(
1243             "black.format_stdin_to_stdout",
1244             return_value=lambda *args, **kwargs: black.Changed.YES,
1245         ) as fsts:
1246             report = MagicMock()
1247             p = "foo.py"
1248             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1249             expected = Path(p)
1250             black.reformat_one(
1251                 path,
1252                 fast=True,
1253                 write_back=black.WriteBack.YES,
1254                 mode=DEFAULT_MODE,
1255                 report=report,
1256             )
1257             fsts.assert_called_once_with(
1258                 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1259             )
1260             # __BLACK_STDIN_FILENAME__ should have been stripped
1261             report.done.assert_called_with(expected, black.Changed.YES)
1262
1263     @pytest.mark.incompatible_with_mypyc
1264     def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1265         with patch(
1266             "black.format_stdin_to_stdout",
1267             return_value=lambda *args, **kwargs: black.Changed.YES,
1268         ) as fsts:
1269             report = MagicMock()
1270             p = "foo.pyi"
1271             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1272             expected = Path(p)
1273             black.reformat_one(
1274                 path,
1275                 fast=True,
1276                 write_back=black.WriteBack.YES,
1277                 mode=DEFAULT_MODE,
1278                 report=report,
1279             )
1280             fsts.assert_called_once_with(
1281                 fast=True,
1282                 write_back=black.WriteBack.YES,
1283                 mode=replace(DEFAULT_MODE, is_pyi=True),
1284             )
1285             # __BLACK_STDIN_FILENAME__ should have been stripped
1286             report.done.assert_called_with(expected, black.Changed.YES)
1287
1288     @pytest.mark.incompatible_with_mypyc
1289     def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1290         with patch(
1291             "black.format_stdin_to_stdout",
1292             return_value=lambda *args, **kwargs: black.Changed.YES,
1293         ) as fsts:
1294             report = MagicMock()
1295             p = "foo.ipynb"
1296             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1297             expected = Path(p)
1298             black.reformat_one(
1299                 path,
1300                 fast=True,
1301                 write_back=black.WriteBack.YES,
1302                 mode=DEFAULT_MODE,
1303                 report=report,
1304             )
1305             fsts.assert_called_once_with(
1306                 fast=True,
1307                 write_back=black.WriteBack.YES,
1308                 mode=replace(DEFAULT_MODE, is_ipynb=True),
1309             )
1310             # __BLACK_STDIN_FILENAME__ should have been stripped
1311             report.done.assert_called_with(expected, black.Changed.YES)
1312
1313     @pytest.mark.incompatible_with_mypyc
1314     def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1315         with patch(
1316             "black.format_stdin_to_stdout",
1317             return_value=lambda *args, **kwargs: black.Changed.YES,
1318         ) as fsts:
1319             report = MagicMock()
1320             # Even with an existing file, since we are forcing stdin, black
1321             # should output to stdout and not modify the file inplace
1322             p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1323             # Make sure is_file actually returns True
1324             self.assertTrue(p.is_file())
1325             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1326             expected = Path(p)
1327             black.reformat_one(
1328                 path,
1329                 fast=True,
1330                 write_back=black.WriteBack.YES,
1331                 mode=DEFAULT_MODE,
1332                 report=report,
1333             )
1334             fsts.assert_called_once()
1335             # __BLACK_STDIN_FILENAME__ should have been stripped
1336             report.done.assert_called_with(expected, black.Changed.YES)
1337
1338     def test_reformat_one_with_stdin_empty(self) -> None:
1339         cases = [
1340             ("", ""),
1341             ("\n", "\n"),
1342             ("\r\n", "\r\n"),
1343             (" \t", ""),
1344             (" \t\n\t ", "\n"),
1345             (" \t\r\n\t ", "\r\n"),
1346         ]
1347
1348         def _new_wrapper(
1349             output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1350         ) -> Callable[[Any, Any], io.TextIOWrapper]:
1351             def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1352                 if args == (sys.stdout.buffer,):
1353                     # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1354                     # return our mock object.
1355                     return output
1356                 # It's something else (i.e. `decode_bytes()`) calling
1357                 # `io.TextIOWrapper()`, pass through to the original implementation.
1358                 # See discussion in https://github.com/psf/black/pull/2489
1359                 return io_TextIOWrapper(*args, **kwargs)
1360
1361             return get_output
1362
1363         mode = black.Mode(preview=True)
1364         for content, expected in cases:
1365             output = io.StringIO()
1366             io_TextIOWrapper = io.TextIOWrapper
1367
1368             with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1369                 try:
1370                     black.format_stdin_to_stdout(
1371                         fast=True,
1372                         content=content,
1373                         write_back=black.WriteBack.YES,
1374                         mode=mode,
1375                     )
1376                 except io.UnsupportedOperation:
1377                     pass  # StringIO does not support detach
1378                 assert output.getvalue() == expected
1379
1380         # An empty string is the only test case for `preview=False`
1381         output = io.StringIO()
1382         io_TextIOWrapper = io.TextIOWrapper
1383         with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1384             try:
1385                 black.format_stdin_to_stdout(
1386                     fast=True,
1387                     content="",
1388                     write_back=black.WriteBack.YES,
1389                     mode=DEFAULT_MODE,
1390                 )
1391             except io.UnsupportedOperation:
1392                 pass  # StringIO does not support detach
1393             assert output.getvalue() == ""
1394
1395     def test_invalid_cli_regex(self) -> None:
1396         for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1397             self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1398
1399     def test_required_version_matches_version(self) -> None:
1400         self.invokeBlack(
1401             ["--required-version", black.__version__, "-c", "0"],
1402             exit_code=0,
1403             ignore_config=True,
1404         )
1405
1406     def test_required_version_matches_partial_version(self) -> None:
1407         self.invokeBlack(
1408             ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1409             exit_code=0,
1410             ignore_config=True,
1411         )
1412
1413     def test_required_version_does_not_match_on_minor_version(self) -> None:
1414         self.invokeBlack(
1415             ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1416             exit_code=1,
1417             ignore_config=True,
1418         )
1419
1420     def test_required_version_does_not_match_version(self) -> None:
1421         result = BlackRunner().invoke(
1422             black.main,
1423             ["--required-version", "20.99b", "-c", "0"],
1424         )
1425         self.assertEqual(result.exit_code, 1)
1426         self.assertIn("required version", result.stderr)
1427
1428     def test_preserves_line_endings(self) -> None:
1429         with TemporaryDirectory() as workspace:
1430             test_file = Path(workspace) / "test.py"
1431             for nl in ["\n", "\r\n"]:
1432                 contents = nl.join(["def f(  ):", "    pass"])
1433                 test_file.write_bytes(contents.encode())
1434                 ff(test_file, write_back=black.WriteBack.YES)
1435                 updated_contents: bytes = test_file.read_bytes()
1436                 self.assertIn(nl.encode(), updated_contents)
1437                 if nl == "\n":
1438                     self.assertNotIn(b"\r\n", updated_contents)
1439
1440     def test_preserves_line_endings_via_stdin(self) -> None:
1441         for nl in ["\n", "\r\n"]:
1442             contents = nl.join(["def f(  ):", "    pass"])
1443             runner = BlackRunner()
1444             result = runner.invoke(
1445                 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1446             )
1447             self.assertEqual(result.exit_code, 0)
1448             output = result.stdout_bytes
1449             self.assertIn(nl.encode("utf8"), output)
1450             if nl == "\n":
1451                 self.assertNotIn(b"\r\n", output)
1452
1453     def test_normalize_line_endings(self) -> None:
1454         with TemporaryDirectory() as workspace:
1455             test_file = Path(workspace) / "test.py"
1456             for data, expected in (
1457                 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1458                 (b"l\nl\r\n ", b"l\nl\n"),
1459             ):
1460                 test_file.write_bytes(data)
1461                 ff(test_file, write_back=black.WriteBack.YES)
1462                 self.assertEqual(test_file.read_bytes(), expected)
1463
1464     def test_assert_equivalent_different_asts(self) -> None:
1465         with self.assertRaises(AssertionError):
1466             black.assert_equivalent("{}", "None")
1467
1468     def test_shhh_click(self) -> None:
1469         try:
1470             from click import _unicodefun  # type: ignore
1471         except ImportError:
1472             self.skipTest("Incompatible Click version")
1473
1474         if not hasattr(_unicodefun, "_verify_python_env"):
1475             self.skipTest("Incompatible Click version")
1476
1477         # First, let's see if Click is crashing with a preferred ASCII charset.
1478         with patch("locale.getpreferredencoding") as gpe:
1479             gpe.return_value = "ASCII"
1480             with self.assertRaises(RuntimeError):
1481                 _unicodefun._verify_python_env()
1482         # Now, let's silence Click...
1483         black.patch_click()
1484         # ...and confirm it's silent.
1485         with patch("locale.getpreferredencoding") as gpe:
1486             gpe.return_value = "ASCII"
1487             try:
1488                 _unicodefun._verify_python_env()
1489             except RuntimeError as re:
1490                 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1491
1492     def test_root_logger_not_used_directly(self) -> None:
1493         def fail(*args: Any, **kwargs: Any) -> None:
1494             self.fail("Record created with root logger")
1495
1496         with patch.multiple(
1497             logging.root,
1498             debug=fail,
1499             info=fail,
1500             warning=fail,
1501             error=fail,
1502             critical=fail,
1503             log=fail,
1504         ):
1505             ff(THIS_DIR / "util.py")
1506
1507     def test_invalid_config_return_code(self) -> None:
1508         tmp_file = Path(black.dump_to_file())
1509         try:
1510             tmp_config = Path(black.dump_to_file())
1511             tmp_config.unlink()
1512             args = ["--config", str(tmp_config), str(tmp_file)]
1513             self.invokeBlack(args, exit_code=2, ignore_config=False)
1514         finally:
1515             tmp_file.unlink()
1516
1517     def test_parse_pyproject_toml(self) -> None:
1518         test_toml_file = THIS_DIR / "test.toml"
1519         config = black.parse_pyproject_toml(str(test_toml_file))
1520         self.assertEqual(config["verbose"], 1)
1521         self.assertEqual(config["check"], "no")
1522         self.assertEqual(config["diff"], "y")
1523         self.assertEqual(config["color"], True)
1524         self.assertEqual(config["line_length"], 79)
1525         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1526         self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1527         self.assertEqual(config["exclude"], r"\.pyi?$")
1528         self.assertEqual(config["include"], r"\.py?$")
1529
1530     def test_parse_pyproject_toml_project_metadata(self) -> None:
1531         for test_toml, expected in [
1532             ("only_black_pyproject.toml", ["py310"]),
1533             ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1534             ("neither_pyproject.toml", None),
1535             ("both_pyproject.toml", ["py310"]),
1536         ]:
1537             test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1538             config = black.parse_pyproject_toml(str(test_toml_file))
1539             self.assertEqual(config.get("target_version"), expected)
1540
1541     def test_infer_target_version(self) -> None:
1542         for version, expected in [
1543             ("3.6", [TargetVersion.PY36]),
1544             ("3.11.0rc1", [TargetVersion.PY311]),
1545             (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
1546             (
1547                 ">=3.10.6",
1548                 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1549             ),
1550             ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1551             (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1552             (
1553                 ">3.7,!=3.8,!=3.9",
1554                 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1555             ),
1556             (
1557                 "> 3.9.4, != 3.10.3",
1558                 [
1559                     TargetVersion.PY39,
1560                     TargetVersion.PY310,
1561                     TargetVersion.PY311,
1562                     TargetVersion.PY312,
1563                 ],
1564             ),
1565             (
1566                 "!=3.3,!=3.4",
1567                 [
1568                     TargetVersion.PY35,
1569                     TargetVersion.PY36,
1570                     TargetVersion.PY37,
1571                     TargetVersion.PY38,
1572                     TargetVersion.PY39,
1573                     TargetVersion.PY310,
1574                     TargetVersion.PY311,
1575                     TargetVersion.PY312,
1576                 ],
1577             ),
1578             (
1579                 "==3.*",
1580                 [
1581                     TargetVersion.PY33,
1582                     TargetVersion.PY34,
1583                     TargetVersion.PY35,
1584                     TargetVersion.PY36,
1585                     TargetVersion.PY37,
1586                     TargetVersion.PY38,
1587                     TargetVersion.PY39,
1588                     TargetVersion.PY310,
1589                     TargetVersion.PY311,
1590                     TargetVersion.PY312,
1591                 ],
1592             ),
1593             ("==3.8.*", [TargetVersion.PY38]),
1594             (None, None),
1595             ("", None),
1596             ("invalid", None),
1597             ("==invalid", None),
1598             (">3.9,!=invalid", None),
1599             ("3", None),
1600             ("3.2", None),
1601             ("2.7.18", None),
1602             ("==2.7", None),
1603             (">3.10,<3.11", None),
1604         ]:
1605             test_toml = {"project": {"requires-python": version}}
1606             result = black.files.infer_target_version(test_toml)
1607             self.assertEqual(result, expected)
1608
1609     def test_read_pyproject_toml(self) -> None:
1610         test_toml_file = THIS_DIR / "test.toml"
1611         fake_ctx = FakeContext()
1612         black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1613         config = fake_ctx.default_map
1614         self.assertEqual(config["verbose"], "1")
1615         self.assertEqual(config["check"], "no")
1616         self.assertEqual(config["diff"], "y")
1617         self.assertEqual(config["color"], "True")
1618         self.assertEqual(config["line_length"], "79")
1619         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1620         self.assertEqual(config["exclude"], r"\.pyi?$")
1621         self.assertEqual(config["include"], r"\.py?$")
1622
1623     @pytest.mark.incompatible_with_mypyc
1624     def test_find_project_root(self) -> None:
1625         with TemporaryDirectory() as workspace:
1626             root = Path(workspace)
1627             test_dir = root / "test"
1628             test_dir.mkdir()
1629
1630             src_dir = root / "src"
1631             src_dir.mkdir()
1632
1633             root_pyproject = root / "pyproject.toml"
1634             root_pyproject.touch()
1635             src_pyproject = src_dir / "pyproject.toml"
1636             src_pyproject.touch()
1637             src_python = src_dir / "foo.py"
1638             src_python.touch()
1639
1640             self.assertEqual(
1641                 black.find_project_root((src_dir, test_dir)),
1642                 (root.resolve(), "pyproject.toml"),
1643             )
1644             self.assertEqual(
1645                 black.find_project_root((src_dir,)),
1646                 (src_dir.resolve(), "pyproject.toml"),
1647             )
1648             self.assertEqual(
1649                 black.find_project_root((src_python,)),
1650                 (src_dir.resolve(), "pyproject.toml"),
1651             )
1652
1653             with change_directory(test_dir):
1654                 self.assertEqual(
1655                     black.find_project_root(("-",), stdin_filename="../src/a.py"),
1656                     (src_dir.resolve(), "pyproject.toml"),
1657                 )
1658
1659     @patch(
1660         "black.files.find_user_pyproject_toml",
1661     )
1662     def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1663         find_user_pyproject_toml.side_effect = RuntimeError()
1664
1665         with redirect_stderr(io.StringIO()) as stderr:
1666             result = black.files.find_pyproject_toml(
1667                 path_search_start=(str(Path.cwd().root),)
1668             )
1669
1670         assert result is None
1671         err = stderr.getvalue()
1672         assert "Ignoring user configuration" in err
1673
1674     @patch(
1675         "black.files.find_user_pyproject_toml",
1676         black.files.find_user_pyproject_toml.__wrapped__,
1677     )
1678     def test_find_user_pyproject_toml_linux(self) -> None:
1679         if system() == "Windows":
1680             return
1681
1682         # Test if XDG_CONFIG_HOME is checked
1683         with TemporaryDirectory() as workspace:
1684             tmp_user_config = Path(workspace) / "black"
1685             with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1686                 self.assertEqual(
1687                     black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1688                 )
1689
1690         # Test fallback for XDG_CONFIG_HOME
1691         with patch.dict("os.environ"):
1692             os.environ.pop("XDG_CONFIG_HOME", None)
1693             fallback_user_config = Path("~/.config").expanduser() / "black"
1694             self.assertEqual(
1695                 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1696             )
1697
1698     def test_find_user_pyproject_toml_windows(self) -> None:
1699         if system() != "Windows":
1700             return
1701
1702         user_config_path = Path.home() / ".black"
1703         self.assertEqual(
1704             black.files.find_user_pyproject_toml(), user_config_path.resolve()
1705         )
1706
1707     def test_bpo_33660_workaround(self) -> None:
1708         if system() == "Windows":
1709             return
1710
1711         # https://bugs.python.org/issue33660
1712         root = Path("/")
1713         with change_directory(root):
1714             path = Path("workspace") / "project"
1715             report = black.Report(verbose=True)
1716             normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1717             self.assertEqual(normalized_path, "workspace/project")
1718
1719     def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1720         if system() != "Windows":
1721             return
1722
1723         with TemporaryDirectory() as workspace:
1724             root = Path(workspace)
1725             junction_dir = root / "junction"
1726             junction_target_outside_of_root = root / ".."
1727             os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1728
1729             report = black.Report(verbose=True)
1730             normalized_path = black.normalize_path_maybe_ignore(
1731                 junction_dir, root, report
1732             )
1733             # Manually delete for Python < 3.8
1734             os.system(f"rmdir {junction_dir}")
1735
1736             self.assertEqual(normalized_path, None)
1737
1738     def test_newline_comment_interaction(self) -> None:
1739         source = "class A:\\\r\n# type: ignore\n pass\n"
1740         output = black.format_str(source, mode=DEFAULT_MODE)
1741         black.assert_stable(source, output, mode=DEFAULT_MODE)
1742
1743     def test_bpo_2142_workaround(self) -> None:
1744         # https://bugs.python.org/issue2142
1745
1746         source, _ = read_data("miscellaneous", "missing_final_newline")
1747         # read_data adds a trailing newline
1748         source = source.rstrip()
1749         expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1750         tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1751         diff_header = re.compile(
1752             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1753             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1754         )
1755         try:
1756             result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1757             self.assertEqual(result.exit_code, 0)
1758         finally:
1759             os.unlink(tmp_file)
1760         actual = result.output
1761         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1762         self.assertEqual(actual, expected)
1763
1764     @staticmethod
1765     def compare_results(
1766         result: click.testing.Result, expected_value: str, expected_exit_code: int
1767     ) -> None:
1768         """Helper method to test the value and exit code of a click Result."""
1769         assert (
1770             result.output == expected_value
1771         ), "The output did not match the expected value."
1772         assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1773
1774     def test_code_option(self) -> None:
1775         """Test the code option with no changes."""
1776         code = 'print("Hello world")\n'
1777         args = ["--code", code]
1778         result = CliRunner().invoke(black.main, args)
1779
1780         self.compare_results(result, code, 0)
1781
1782     def test_code_option_changed(self) -> None:
1783         """Test the code option when changes are required."""
1784         code = "print('hello world')"
1785         formatted = black.format_str(code, mode=DEFAULT_MODE)
1786
1787         args = ["--code", code]
1788         result = CliRunner().invoke(black.main, args)
1789
1790         self.compare_results(result, formatted, 0)
1791
1792     def test_code_option_check(self) -> None:
1793         """Test the code option when check is passed."""
1794         args = ["--check", "--code", 'print("Hello world")\n']
1795         result = CliRunner().invoke(black.main, args)
1796         self.compare_results(result, "", 0)
1797
1798     def test_code_option_check_changed(self) -> None:
1799         """Test the code option when changes are required, and check is passed."""
1800         args = ["--check", "--code", "print('hello world')"]
1801         result = CliRunner().invoke(black.main, args)
1802         self.compare_results(result, "", 1)
1803
1804     def test_code_option_diff(self) -> None:
1805         """Test the code option when diff is passed."""
1806         code = "print('hello world')"
1807         formatted = black.format_str(code, mode=DEFAULT_MODE)
1808         result_diff = diff(code, formatted, "STDIN", "STDOUT")
1809
1810         args = ["--diff", "--code", code]
1811         result = CliRunner().invoke(black.main, args)
1812
1813         # Remove time from diff
1814         output = DIFF_TIME.sub("", result.output)
1815
1816         assert output == result_diff, "The output did not match the expected value."
1817         assert result.exit_code == 0, "The exit code is incorrect."
1818
1819     def test_code_option_color_diff(self) -> None:
1820         """Test the code option when color and diff are passed."""
1821         code = "print('hello world')"
1822         formatted = black.format_str(code, mode=DEFAULT_MODE)
1823
1824         result_diff = diff(code, formatted, "STDIN", "STDOUT")
1825         result_diff = color_diff(result_diff)
1826
1827         args = ["--diff", "--color", "--code", code]
1828         result = CliRunner().invoke(black.main, args)
1829
1830         # Remove time from diff
1831         output = DIFF_TIME.sub("", result.output)
1832
1833         assert output == result_diff, "The output did not match the expected value."
1834         assert result.exit_code == 0, "The exit code is incorrect."
1835
1836     @pytest.mark.incompatible_with_mypyc
1837     def test_code_option_safe(self) -> None:
1838         """Test that the code option throws an error when the sanity checks fail."""
1839         # Patch black.assert_equivalent to ensure the sanity checks fail
1840         with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1841             code = 'print("Hello world")'
1842             error_msg = f"{code}\nerror: cannot format <string>: \n"
1843
1844             args = ["--safe", "--code", code]
1845             result = CliRunner().invoke(black.main, args)
1846
1847             self.compare_results(result, error_msg, 123)
1848
1849     def test_code_option_fast(self) -> None:
1850         """Test that the code option ignores errors when the sanity checks fail."""
1851         # Patch black.assert_equivalent to ensure the sanity checks fail
1852         with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1853             code = 'print("Hello world")'
1854             formatted = black.format_str(code, mode=DEFAULT_MODE)
1855
1856             args = ["--fast", "--code", code]
1857             result = CliRunner().invoke(black.main, args)
1858
1859             self.compare_results(result, formatted, 0)
1860
1861     @pytest.mark.incompatible_with_mypyc
1862     def test_code_option_config(self) -> None:
1863         """
1864         Test that the code option finds the pyproject.toml in the current directory.
1865         """
1866         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1867             args = ["--code", "print"]
1868             # This is the only directory known to contain a pyproject.toml
1869             with change_directory(PROJECT_ROOT):
1870                 CliRunner().invoke(black.main, args)
1871                 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1872
1873             assert (
1874                 len(parse.mock_calls) >= 1
1875             ), "Expected config parse to be called with the current directory."
1876
1877             _, call_args, _ = parse.mock_calls[0]
1878             assert (
1879                 call_args[0].lower() == str(pyproject_path).lower()
1880             ), "Incorrect config loaded."
1881
1882     @pytest.mark.incompatible_with_mypyc
1883     def test_code_option_parent_config(self) -> None:
1884         """
1885         Test that the code option finds the pyproject.toml in the parent directory.
1886         """
1887         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1888             with change_directory(THIS_DIR):
1889                 args = ["--code", "print"]
1890                 CliRunner().invoke(black.main, args)
1891
1892                 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1893                 assert (
1894                     len(parse.mock_calls) >= 1
1895                 ), "Expected config parse to be called with the current directory."
1896
1897                 _, call_args, _ = parse.mock_calls[0]
1898                 assert (
1899                     call_args[0].lower() == str(pyproject_path).lower()
1900                 ), "Incorrect config loaded."
1901
1902     def test_for_handled_unexpected_eof_error(self) -> None:
1903         """
1904         Test that an unexpected EOF SyntaxError is nicely presented.
1905         """
1906         with pytest.raises(black.parsing.InvalidInput) as exc_info:
1907             black.lib2to3_parse("print(", {})
1908
1909         exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1910
1911     def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1912         with pytest.raises(AssertionError) as err:
1913             black.assert_equivalent("a«»a  = 1", "a«»a  = 1")
1914
1915         err.match("--safe")
1916         # Unfortunately the SyntaxError message has changed in newer versions so we
1917         # can't match it directly.
1918         err.match("invalid character")
1919         err.match(r"\(<unknown>, line 1\)")
1920
1921
1922 class TestCaching:
1923     def test_get_cache_dir(
1924         self,
1925         tmp_path: Path,
1926         monkeypatch: pytest.MonkeyPatch,
1927     ) -> None:
1928         # Create multiple cache directories
1929         workspace1 = tmp_path / "ws1"
1930         workspace1.mkdir()
1931         workspace2 = tmp_path / "ws2"
1932         workspace2.mkdir()
1933
1934         # Force user_cache_dir to use the temporary directory for easier assertions
1935         patch_user_cache_dir = patch(
1936             target="black.cache.user_cache_dir",
1937             autospec=True,
1938             return_value=str(workspace1),
1939         )
1940
1941         # If BLACK_CACHE_DIR is not set, use user_cache_dir
1942         monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1943         with patch_user_cache_dir:
1944             assert get_cache_dir() == workspace1
1945
1946         # If it is set, use the path provided in the env var.
1947         monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1948         assert get_cache_dir() == workspace2
1949
1950     def test_cache_broken_file(self) -> None:
1951         mode = DEFAULT_MODE
1952         with cache_dir() as workspace:
1953             cache_file = get_cache_file(mode)
1954             cache_file.write_text("this is not a pickle")
1955             assert black.read_cache(mode) == {}
1956             src = (workspace / "test.py").resolve()
1957             src.write_text("print('hello')")
1958             invokeBlack([str(src)])
1959             cache = black.read_cache(mode)
1960             assert str(src) in cache
1961
1962     def test_cache_single_file_already_cached(self) -> None:
1963         mode = DEFAULT_MODE
1964         with cache_dir() as workspace:
1965             src = (workspace / "test.py").resolve()
1966             src.write_text("print('hello')")
1967             black.write_cache({}, [src], mode)
1968             invokeBlack([str(src)])
1969             assert src.read_text() == "print('hello')"
1970
1971     @event_loop()
1972     def test_cache_multiple_files(self) -> None:
1973         mode = DEFAULT_MODE
1974         with cache_dir() as workspace, patch(
1975             "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1976         ):
1977             one = (workspace / "one.py").resolve()
1978             with one.open("w") as fobj:
1979                 fobj.write("print('hello')")
1980             two = (workspace / "two.py").resolve()
1981             with two.open("w") as fobj:
1982                 fobj.write("print('hello')")
1983             black.write_cache({}, [one], mode)
1984             invokeBlack([str(workspace)])
1985             with one.open("r") as fobj:
1986                 assert fobj.read() == "print('hello')"
1987             with two.open("r") as fobj:
1988                 assert fobj.read() == 'print("hello")\n'
1989             cache = black.read_cache(mode)
1990             assert str(one) in cache
1991             assert str(two) in cache
1992
1993     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1994     def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1995         mode = DEFAULT_MODE
1996         with cache_dir() as workspace:
1997             src = (workspace / "test.py").resolve()
1998             with src.open("w") as fobj:
1999                 fobj.write("print('hello')")
2000             with patch("black.read_cache") as read_cache, patch(
2001                 "black.write_cache"
2002             ) as write_cache:
2003                 cmd = [str(src), "--diff"]
2004                 if color:
2005                     cmd.append("--color")
2006                 invokeBlack(cmd)
2007                 cache_file = get_cache_file(mode)
2008                 assert cache_file.exists() is False
2009                 write_cache.assert_not_called()
2010                 read_cache.assert_not_called()
2011
2012     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2013     @event_loop()
2014     def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2015         with cache_dir() as workspace:
2016             for tag in range(0, 4):
2017                 src = (workspace / f"test{tag}.py").resolve()
2018                 with src.open("w") as fobj:
2019                     fobj.write("print('hello')")
2020             with patch(
2021                 "black.concurrency.Manager", wraps=multiprocessing.Manager
2022             ) as mgr:
2023                 cmd = ["--diff", str(workspace)]
2024                 if color:
2025                     cmd.append("--color")
2026                 invokeBlack(cmd, exit_code=0)
2027                 # this isn't quite doing what we want, but if it _isn't_
2028                 # called then we cannot be using the lock it provides
2029                 mgr.assert_called()
2030
2031     def test_no_cache_when_stdin(self) -> None:
2032         mode = DEFAULT_MODE
2033         with cache_dir():
2034             result = CliRunner().invoke(
2035                 black.main, ["-"], input=BytesIO(b"print('hello')")
2036             )
2037             assert not result.exit_code
2038             cache_file = get_cache_file(mode)
2039             assert not cache_file.exists()
2040
2041     def test_read_cache_no_cachefile(self) -> None:
2042         mode = DEFAULT_MODE
2043         with cache_dir():
2044             assert black.read_cache(mode) == {}
2045
2046     def test_write_cache_read_cache(self) -> None:
2047         mode = DEFAULT_MODE
2048         with cache_dir() as workspace:
2049             src = (workspace / "test.py").resolve()
2050             src.touch()
2051             black.write_cache({}, [src], mode)
2052             cache = black.read_cache(mode)
2053             assert str(src) in cache
2054             assert cache[str(src)] == black.get_cache_info(src)
2055
2056     def test_filter_cached(self) -> None:
2057         with TemporaryDirectory() as workspace:
2058             path = Path(workspace)
2059             uncached = (path / "uncached").resolve()
2060             cached = (path / "cached").resolve()
2061             cached_but_changed = (path / "changed").resolve()
2062             uncached.touch()
2063             cached.touch()
2064             cached_but_changed.touch()
2065             cache = {
2066                 str(cached): black.get_cache_info(cached),
2067                 str(cached_but_changed): (0.0, 0),
2068             }
2069             todo, done = black.cache.filter_cached(
2070                 cache, {uncached, cached, cached_but_changed}
2071             )
2072             assert todo == {uncached, cached_but_changed}
2073             assert done == {cached}
2074
2075     def test_write_cache_creates_directory_if_needed(self) -> None:
2076         mode = DEFAULT_MODE
2077         with cache_dir(exists=False) as workspace:
2078             assert not workspace.exists()
2079             black.write_cache({}, [], mode)
2080             assert workspace.exists()
2081
2082     @event_loop()
2083     def test_failed_formatting_does_not_get_cached(self) -> None:
2084         mode = DEFAULT_MODE
2085         with cache_dir() as workspace, patch(
2086             "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2087         ):
2088             failing = (workspace / "failing.py").resolve()
2089             with failing.open("w") as fobj:
2090                 fobj.write("not actually python")
2091             clean = (workspace / "clean.py").resolve()
2092             with clean.open("w") as fobj:
2093                 fobj.write('print("hello")\n')
2094             invokeBlack([str(workspace)], exit_code=123)
2095             cache = black.read_cache(mode)
2096             assert str(failing) not in cache
2097             assert str(clean) in cache
2098
2099     def test_write_cache_write_fail(self) -> None:
2100         mode = DEFAULT_MODE
2101         with cache_dir(), patch.object(Path, "open") as mock:
2102             mock.side_effect = OSError
2103             black.write_cache({}, [], mode)
2104
2105     def test_read_cache_line_lengths(self) -> None:
2106         mode = DEFAULT_MODE
2107         short_mode = replace(DEFAULT_MODE, line_length=1)
2108         with cache_dir() as workspace:
2109             path = (workspace / "file.py").resolve()
2110             path.touch()
2111             black.write_cache({}, [path], mode)
2112             one = black.read_cache(mode)
2113             assert str(path) in one
2114             two = black.read_cache(short_mode)
2115             assert str(path) not in two
2116
2117
2118 def assert_collected_sources(
2119     src: Sequence[Union[str, Path]],
2120     expected: Sequence[Union[str, Path]],
2121     *,
2122     ctx: Optional[FakeContext] = None,
2123     exclude: Optional[str] = None,
2124     include: Optional[str] = None,
2125     extend_exclude: Optional[str] = None,
2126     force_exclude: Optional[str] = None,
2127     stdin_filename: Optional[str] = None,
2128 ) -> None:
2129     gs_src = tuple(str(Path(s)) for s in src)
2130     gs_expected = [Path(s) for s in expected]
2131     gs_exclude = None if exclude is None else compile_pattern(exclude)
2132     gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2133     gs_extend_exclude = (
2134         None if extend_exclude is None else compile_pattern(extend_exclude)
2135     )
2136     gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2137     collected = black.get_sources(
2138         ctx=ctx or FakeContext(),
2139         src=gs_src,
2140         quiet=False,
2141         verbose=False,
2142         include=gs_include,
2143         exclude=gs_exclude,
2144         extend_exclude=gs_extend_exclude,
2145         force_exclude=gs_force_exclude,
2146         report=black.Report(),
2147         stdin_filename=stdin_filename,
2148     )
2149     assert sorted(collected) == sorted(gs_expected)
2150
2151
2152 class TestFileCollection:
2153     def test_include_exclude(self) -> None:
2154         path = THIS_DIR / "data" / "include_exclude_tests"
2155         src = [path]
2156         expected = [
2157             Path(path / "b/dont_exclude/a.py"),
2158             Path(path / "b/dont_exclude/a.pyi"),
2159         ]
2160         assert_collected_sources(
2161             src,
2162             expected,
2163             include=r"\.pyi?$",
2164             exclude=r"/exclude/|/\.definitely_exclude/",
2165         )
2166
2167     def test_gitignore_used_as_default(self) -> None:
2168         base = Path(DATA_DIR / "include_exclude_tests")
2169         expected = [
2170             base / "b/.definitely_exclude/a.py",
2171             base / "b/.definitely_exclude/a.pyi",
2172         ]
2173         src = [base / "b/"]
2174         ctx = FakeContext()
2175         ctx.obj["root"] = base
2176         assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2177
2178     def test_gitignore_used_on_multiple_sources(self) -> None:
2179         root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2180         expected = [
2181             root / "dir1" / "b.py",
2182             root / "dir2" / "b.py",
2183         ]
2184         ctx = FakeContext()
2185         ctx.obj["root"] = root
2186         src = [root / "dir1", root / "dir2"]
2187         assert_collected_sources(src, expected, ctx=ctx)
2188
2189     @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2190     def test_exclude_for_issue_1572(self) -> None:
2191         # Exclude shouldn't touch files that were explicitly given to Black through the
2192         # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2193         # https://github.com/psf/black/issues/1572
2194         path = DATA_DIR / "include_exclude_tests"
2195         src = [path / "b/exclude/a.py"]
2196         expected = [path / "b/exclude/a.py"]
2197         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2198
2199     def test_gitignore_exclude(self) -> None:
2200         path = THIS_DIR / "data" / "include_exclude_tests"
2201         include = re.compile(r"\.pyi?$")
2202         exclude = re.compile(r"")
2203         report = black.Report()
2204         gitignore = PathSpec.from_lines(
2205             "gitwildmatch", ["exclude/", ".definitely_exclude"]
2206         )
2207         sources: List[Path] = []
2208         expected = [
2209             Path(path / "b/dont_exclude/a.py"),
2210             Path(path / "b/dont_exclude/a.pyi"),
2211         ]
2212         this_abs = THIS_DIR.resolve()
2213         sources.extend(
2214             black.gen_python_files(
2215                 path.iterdir(),
2216                 this_abs,
2217                 include,
2218                 exclude,
2219                 None,
2220                 None,
2221                 report,
2222                 {path: gitignore},
2223                 verbose=False,
2224                 quiet=False,
2225             )
2226         )
2227         assert sorted(expected) == sorted(sources)
2228
2229     def test_nested_gitignore(self) -> None:
2230         path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2231         include = re.compile(r"\.pyi?$")
2232         exclude = re.compile(r"")
2233         root_gitignore = black.files.get_gitignore(path)
2234         report = black.Report()
2235         expected: List[Path] = [
2236             Path(path / "x.py"),
2237             Path(path / "root/b.py"),
2238             Path(path / "root/c.py"),
2239             Path(path / "root/child/c.py"),
2240         ]
2241         this_abs = THIS_DIR.resolve()
2242         sources = list(
2243             black.gen_python_files(
2244                 path.iterdir(),
2245                 this_abs,
2246                 include,
2247                 exclude,
2248                 None,
2249                 None,
2250                 report,
2251                 {path: root_gitignore},
2252                 verbose=False,
2253                 quiet=False,
2254             )
2255         )
2256         assert sorted(expected) == sorted(sources)
2257
2258     def test_nested_gitignore_directly_in_source_directory(self) -> None:
2259         # https://github.com/psf/black/issues/2598
2260         path = Path(DATA_DIR / "nested_gitignore_tests")
2261         src = Path(path / "root" / "child")
2262         expected = [src / "a.py", src / "c.py"]
2263         assert_collected_sources([src], expected)
2264
2265     def test_invalid_gitignore(self) -> None:
2266         path = THIS_DIR / "data" / "invalid_gitignore_tests"
2267         empty_config = path / "pyproject.toml"
2268         result = BlackRunner().invoke(
2269             black.main, ["--verbose", "--config", str(empty_config), str(path)]
2270         )
2271         assert result.exit_code == 1
2272         assert result.stderr_bytes is not None
2273
2274         gitignore = path / ".gitignore"
2275         assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2276
2277     def test_invalid_nested_gitignore(self) -> None:
2278         path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2279         empty_config = path / "pyproject.toml"
2280         result = BlackRunner().invoke(
2281             black.main, ["--verbose", "--config", str(empty_config), str(path)]
2282         )
2283         assert result.exit_code == 1
2284         assert result.stderr_bytes is not None
2285
2286         gitignore = path / "a" / ".gitignore"
2287         assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2288
2289     def test_gitignore_that_ignores_subfolders(self) -> None:
2290         # If gitignore with */* is in root
2291         root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2292         expected = [root / "b.py"]
2293         ctx = FakeContext()
2294         ctx.obj["root"] = root
2295         assert_collected_sources([root], expected, ctx=ctx)
2296
2297         # If .gitignore with */* is nested
2298         root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2299         expected = [
2300             root / "a.py",
2301             root / "subdir" / "b.py",
2302         ]
2303         ctx = FakeContext()
2304         ctx.obj["root"] = root
2305         assert_collected_sources([root], expected, ctx=ctx)
2306
2307         # If command is executed from outer dir
2308         root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2309         target = root / "subdir"
2310         expected = [target / "b.py"]
2311         ctx = FakeContext()
2312         ctx.obj["root"] = root
2313         assert_collected_sources([target], expected, ctx=ctx)
2314
2315     def test_empty_include(self) -> None:
2316         path = DATA_DIR / "include_exclude_tests"
2317         src = [path]
2318         expected = [
2319             Path(path / "b/exclude/a.pie"),
2320             Path(path / "b/exclude/a.py"),
2321             Path(path / "b/exclude/a.pyi"),
2322             Path(path / "b/dont_exclude/a.pie"),
2323             Path(path / "b/dont_exclude/a.py"),
2324             Path(path / "b/dont_exclude/a.pyi"),
2325             Path(path / "b/.definitely_exclude/a.pie"),
2326             Path(path / "b/.definitely_exclude/a.py"),
2327             Path(path / "b/.definitely_exclude/a.pyi"),
2328             Path(path / ".gitignore"),
2329             Path(path / "pyproject.toml"),
2330         ]
2331         # Setting exclude explicitly to an empty string to block .gitignore usage.
2332         assert_collected_sources(src, expected, include="", exclude="")
2333
2334     def test_extend_exclude(self) -> None:
2335         path = DATA_DIR / "include_exclude_tests"
2336         src = [path]
2337         expected = [
2338             Path(path / "b/exclude/a.py"),
2339             Path(path / "b/dont_exclude/a.py"),
2340         ]
2341         assert_collected_sources(
2342             src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2343         )
2344
2345     @pytest.mark.incompatible_with_mypyc
2346     def test_symlink_out_of_root_directory(self) -> None:
2347         path = MagicMock()
2348         root = THIS_DIR.resolve()
2349         child = MagicMock()
2350         include = re.compile(black.DEFAULT_INCLUDES)
2351         exclude = re.compile(black.DEFAULT_EXCLUDES)
2352         report = black.Report()
2353         gitignore = PathSpec.from_lines("gitwildmatch", [])
2354         # `child` should behave like a symlink which resolved path is clearly
2355         # outside of the `root` directory.
2356         path.iterdir.return_value = [child]
2357         child.resolve.return_value = Path("/a/b/c")
2358         child.as_posix.return_value = "/a/b/c"
2359         try:
2360             list(
2361                 black.gen_python_files(
2362                     path.iterdir(),
2363                     root,
2364                     include,
2365                     exclude,
2366                     None,
2367                     None,
2368                     report,
2369                     {path: gitignore},
2370                     verbose=False,
2371                     quiet=False,
2372                 )
2373             )
2374         except ValueError as ve:
2375             pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2376         path.iterdir.assert_called_once()
2377         child.resolve.assert_called_once()
2378
2379     @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2380     def test_get_sources_with_stdin(self) -> None:
2381         src = ["-"]
2382         expected = ["-"]
2383         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2384
2385     @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2386     def test_get_sources_with_stdin_filename(self) -> None:
2387         src = ["-"]
2388         stdin_filename = str(THIS_DIR / "data/collections.py")
2389         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2390         assert_collected_sources(
2391             src,
2392             expected,
2393             exclude=r"/exclude/a\.py",
2394             stdin_filename=stdin_filename,
2395         )
2396
2397     @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2398     def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2399         # Exclude shouldn't exclude stdin_filename since it is mimicking the
2400         # file being passed directly. This is the same as
2401         # test_exclude_for_issue_1572
2402         path = DATA_DIR / "include_exclude_tests"
2403         src = ["-"]
2404         stdin_filename = str(path / "b/exclude/a.py")
2405         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2406         assert_collected_sources(
2407             src,
2408             expected,
2409             exclude=r"/exclude/|a\.py",
2410             stdin_filename=stdin_filename,
2411         )
2412
2413     @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2414     def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2415         # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2416         # file being passed directly. This is the same as
2417         # test_exclude_for_issue_1572
2418         src = ["-"]
2419         path = THIS_DIR / "data" / "include_exclude_tests"
2420         stdin_filename = str(path / "b/exclude/a.py")
2421         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2422         assert_collected_sources(
2423             src,
2424             expected,
2425             extend_exclude=r"/exclude/|a\.py",
2426             stdin_filename=stdin_filename,
2427         )
2428
2429     @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2430     def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2431         # Force exclude should exclude the file when passing it through
2432         # stdin_filename
2433         path = THIS_DIR / "data" / "include_exclude_tests"
2434         stdin_filename = str(path / "b/exclude/a.py")
2435         assert_collected_sources(
2436             src=["-"],
2437             expected=[],
2438             force_exclude=r"/exclude/|a\.py",
2439             stdin_filename=stdin_filename,
2440         )
2441
2442
2443 try:
2444     with open(black.__file__, "r", encoding="utf-8") as _bf:
2445         black_source_lines = _bf.readlines()
2446 except UnicodeDecodeError:
2447     if not black.COMPILED:
2448         raise
2449
2450
2451 def tracefunc(
2452     frame: types.FrameType, event: str, arg: Any
2453 ) -> Callable[[types.FrameType, str, Any], Any]:
2454     """Show function calls `from black/__init__.py` as they happen.
2455
2456     Register this with `sys.settrace()` in a test you're debugging.
2457     """
2458     if event != "call":
2459         return tracefunc
2460
2461     stack = len(inspect.stack()) - 19
2462     stack *= 2
2463     filename = frame.f_code.co_filename
2464     lineno = frame.f_lineno
2465     func_sig_lineno = lineno - 1
2466     funcname = black_source_lines[func_sig_lineno].strip()
2467     while funcname.startswith("@"):
2468         func_sig_lineno += 1
2469         funcname = black_source_lines[func_sig_lineno].strip()
2470     if "black/__init__.py" in filename:
2471         print(f"{' ' * stack}{lineno}:{funcname}")
2472     return tracefunc