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

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