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

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