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

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