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

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