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

beb56cf1fdb24bde7256366f0aa1ebf7acb23f60
[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 sys
10 import types
11 import unittest
12 from concurrent.futures import ThreadPoolExecutor
13 from contextlib import contextmanager
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     TypeVar,
28     Union,
29 )
30 from unittest.mock import MagicMock, patch
31
32 import click
33 import pytest
34 import regex as re
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 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     PY36_VERSIONS,
54     THIS_DIR,
55     BlackBaseTestCase,
56     assert_format,
57     change_directory,
58     dump_to_stderr,
59     ff,
60     fs,
61     read_data,
62 )
63
64 THIS_FILE = Path(__file__)
65 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
66 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
67 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
68 T = TypeVar("T")
69 R = TypeVar("R")
70
71 # Match the time output in a diff, but nothing else
72 DIFF_TIME = re.compile(r"\t[\d-:+\. ]+")
73
74
75 @contextmanager
76 def cache_dir(exists: bool = True) -> Iterator[Path]:
77     with TemporaryDirectory() as workspace:
78         cache_dir = Path(workspace)
79         if not exists:
80             cache_dir = cache_dir / "new"
81         with patch("black.cache.CACHE_DIR", cache_dir):
82             yield cache_dir
83
84
85 @contextmanager
86 def event_loop() -> Iterator[None]:
87     policy = asyncio.get_event_loop_policy()
88     loop = policy.new_event_loop()
89     asyncio.set_event_loop(loop)
90     try:
91         yield
92
93     finally:
94         loop.close()
95
96
97 class FakeContext(click.Context):
98     """A fake click Context for when calling functions that need it."""
99
100     def __init__(self) -> None:
101         self.default_map: Dict[str, Any] = {}
102
103
104 class FakeParameter(click.Parameter):
105     """A fake click Parameter for when calling functions that need it."""
106
107     def __init__(self) -> None:
108         pass
109
110
111 class BlackRunner(CliRunner):
112     """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
113
114     def __init__(self) -> None:
115         super().__init__(mix_stderr=False)
116
117
118 def invokeBlack(
119     args: List[str], exit_code: int = 0, ignore_config: bool = True
120 ) -> None:
121     runner = BlackRunner()
122     if ignore_config:
123         args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
124     result = runner.invoke(black.main, args)
125     assert result.stdout_bytes is not None
126     assert result.stderr_bytes is not None
127     msg = (
128         f"Failed with args: {args}\n"
129         f"stdout: {result.stdout_bytes.decode()!r}\n"
130         f"stderr: {result.stderr_bytes.decode()!r}\n"
131         f"exception: {result.exception}"
132     )
133     assert result.exit_code == exit_code, msg
134
135
136 class BlackTestCase(BlackBaseTestCase):
137     invokeBlack = staticmethod(invokeBlack)
138
139     def test_empty_ff(self) -> None:
140         expected = ""
141         tmp_file = Path(black.dump_to_file())
142         try:
143             self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
144             with open(tmp_file, encoding="utf8") as f:
145                 actual = f.read()
146         finally:
147             os.unlink(tmp_file)
148         self.assertFormatEqual(expected, actual)
149
150     def test_piping(self) -> None:
151         source, expected = read_data("src/black/__init__", data=False)
152         result = BlackRunner().invoke(
153             black.main,
154             ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
155             input=BytesIO(source.encode("utf8")),
156         )
157         self.assertEqual(result.exit_code, 0)
158         self.assertFormatEqual(expected, result.output)
159         if source != result.output:
160             black.assert_equivalent(source, result.output)
161             black.assert_stable(source, result.output, DEFAULT_MODE)
162
163     def test_piping_diff(self) -> None:
164         diff_header = re.compile(
165             r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
166             r"\+\d\d\d\d"
167         )
168         source, _ = read_data("expression.py")
169         expected, _ = read_data("expression.diff")
170         config = THIS_DIR / "data" / "empty_pyproject.toml"
171         args = [
172             "-",
173             "--fast",
174             f"--line-length={black.DEFAULT_LINE_LENGTH}",
175             "--diff",
176             f"--config={config}",
177         ]
178         result = BlackRunner().invoke(
179             black.main, args, input=BytesIO(source.encode("utf8"))
180         )
181         self.assertEqual(result.exit_code, 0)
182         actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
183         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
184         self.assertEqual(expected, actual)
185
186     def test_piping_diff_with_color(self) -> None:
187         source, _ = read_data("expression.py")
188         config = THIS_DIR / "data" / "empty_pyproject.toml"
189         args = [
190             "-",
191             "--fast",
192             f"--line-length={black.DEFAULT_LINE_LENGTH}",
193             "--diff",
194             "--color",
195             f"--config={config}",
196         ]
197         result = BlackRunner().invoke(
198             black.main, args, input=BytesIO(source.encode("utf8"))
199         )
200         actual = result.output
201         # Again, the contents are checked in a different test, so only look for colors.
202         self.assertIn("\033[1;37m", actual)
203         self.assertIn("\033[36m", actual)
204         self.assertIn("\033[32m", actual)
205         self.assertIn("\033[31m", actual)
206         self.assertIn("\033[0m", actual)
207
208     @patch("black.dump_to_file", dump_to_stderr)
209     def _test_wip(self) -> None:
210         source, expected = read_data("wip")
211         sys.settrace(tracefunc)
212         mode = replace(
213             DEFAULT_MODE,
214             experimental_string_processing=False,
215             target_versions={black.TargetVersion.PY38},
216         )
217         actual = fs(source, mode=mode)
218         sys.settrace(None)
219         self.assertFormatEqual(expected, actual)
220         black.assert_equivalent(source, actual)
221         black.assert_stable(source, actual, black.FileMode())
222
223     @unittest.expectedFailure
224     @patch("black.dump_to_file", dump_to_stderr)
225     def test_trailing_comma_optional_parens_stability1(self) -> None:
226         source, _expected = read_data("trailing_comma_optional_parens1")
227         actual = fs(source)
228         black.assert_stable(source, actual, DEFAULT_MODE)
229
230     @unittest.expectedFailure
231     @patch("black.dump_to_file", dump_to_stderr)
232     def test_trailing_comma_optional_parens_stability2(self) -> None:
233         source, _expected = read_data("trailing_comma_optional_parens2")
234         actual = fs(source)
235         black.assert_stable(source, actual, DEFAULT_MODE)
236
237     @unittest.expectedFailure
238     @patch("black.dump_to_file", dump_to_stderr)
239     def test_trailing_comma_optional_parens_stability3(self) -> None:
240         source, _expected = read_data("trailing_comma_optional_parens3")
241         actual = fs(source)
242         black.assert_stable(source, actual, DEFAULT_MODE)
243
244     @patch("black.dump_to_file", dump_to_stderr)
245     def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
246         source, _expected = read_data("trailing_comma_optional_parens1")
247         actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
248         black.assert_stable(source, actual, DEFAULT_MODE)
249
250     @patch("black.dump_to_file", dump_to_stderr)
251     def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
252         source, _expected = read_data("trailing_comma_optional_parens2")
253         actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
254         black.assert_stable(source, actual, DEFAULT_MODE)
255
256     @patch("black.dump_to_file", dump_to_stderr)
257     def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
258         source, _expected = read_data("trailing_comma_optional_parens3")
259         actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
260         black.assert_stable(source, actual, DEFAULT_MODE)
261
262     def test_pep_572_version_detection(self) -> None:
263         source, _ = read_data("pep_572")
264         root = black.lib2to3_parse(source)
265         features = black.get_features_used(root)
266         self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
267         versions = black.detect_target_versions(root)
268         self.assertIn(black.TargetVersion.PY38, versions)
269
270     def test_expression_ff(self) -> None:
271         source, expected = read_data("expression")
272         tmp_file = Path(black.dump_to_file(source))
273         try:
274             self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
275             with open(tmp_file, encoding="utf8") as f:
276                 actual = f.read()
277         finally:
278             os.unlink(tmp_file)
279         self.assertFormatEqual(expected, actual)
280         with patch("black.dump_to_file", dump_to_stderr):
281             black.assert_equivalent(source, actual)
282             black.assert_stable(source, actual, DEFAULT_MODE)
283
284     def test_expression_diff(self) -> None:
285         source, _ = read_data("expression.py")
286         config = THIS_DIR / "data" / "empty_pyproject.toml"
287         expected, _ = read_data("expression.diff")
288         tmp_file = Path(black.dump_to_file(source))
289         diff_header = re.compile(
290             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
291             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
292         )
293         try:
294             result = BlackRunner().invoke(
295                 black.main, ["--diff", str(tmp_file), f"--config={config}"]
296             )
297             self.assertEqual(result.exit_code, 0)
298         finally:
299             os.unlink(tmp_file)
300         actual = result.output
301         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
302         if expected != actual:
303             dump = black.dump_to_file(actual)
304             msg = (
305                 "Expected diff isn't equal to the actual. If you made changes to"
306                 " expression.py and this is an anticipated difference, overwrite"
307                 f" tests/data/expression.diff with {dump}"
308             )
309             self.assertEqual(expected, actual, msg)
310
311     def test_expression_diff_with_color(self) -> None:
312         source, _ = read_data("expression.py")
313         config = THIS_DIR / "data" / "empty_pyproject.toml"
314         expected, _ = read_data("expression.diff")
315         tmp_file = Path(black.dump_to_file(source))
316         try:
317             result = BlackRunner().invoke(
318                 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
319             )
320         finally:
321             os.unlink(tmp_file)
322         actual = result.output
323         # We check the contents of the diff in `test_expression_diff`. All
324         # we need to check here is that color codes exist in the result.
325         self.assertIn("\033[1;37m", actual)
326         self.assertIn("\033[36m", actual)
327         self.assertIn("\033[32m", actual)
328         self.assertIn("\033[31m", actual)
329         self.assertIn("\033[0m", actual)
330
331     def test_detect_pos_only_arguments(self) -> None:
332         source, _ = read_data("pep_570")
333         root = black.lib2to3_parse(source)
334         features = black.get_features_used(root)
335         self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
336         versions = black.detect_target_versions(root)
337         self.assertIn(black.TargetVersion.PY38, versions)
338
339     @patch("black.dump_to_file", dump_to_stderr)
340     def test_string_quotes(self) -> None:
341         source, expected = read_data("string_quotes")
342         mode = black.Mode(experimental_string_processing=True)
343         assert_format(source, expected, mode)
344         mode = replace(mode, string_normalization=False)
345         not_normalized = fs(source, mode=mode)
346         self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
347         black.assert_equivalent(source, not_normalized)
348         black.assert_stable(source, not_normalized, mode=mode)
349
350     def test_skip_magic_trailing_comma(self) -> None:
351         source, _ = read_data("expression.py")
352         expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
353         tmp_file = Path(black.dump_to_file(source))
354         diff_header = re.compile(
355             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
356             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
357         )
358         try:
359             result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
360             self.assertEqual(result.exit_code, 0)
361         finally:
362             os.unlink(tmp_file)
363         actual = result.output
364         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
365         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
366         if expected != actual:
367             dump = black.dump_to_file(actual)
368             msg = (
369                 "Expected diff isn't equal to the actual. If you made changes to"
370                 " expression.py and this is an anticipated difference, overwrite"
371                 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
372             )
373             self.assertEqual(expected, actual, msg)
374
375     @patch("black.dump_to_file", dump_to_stderr)
376     def test_async_as_identifier(self) -> None:
377         source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
378         source, expected = read_data("async_as_identifier")
379         actual = fs(source)
380         self.assertFormatEqual(expected, actual)
381         major, minor = sys.version_info[:2]
382         if major < 3 or (major <= 3 and minor < 7):
383             black.assert_equivalent(source, actual)
384         black.assert_stable(source, actual, DEFAULT_MODE)
385         # ensure black can parse this when the target is 3.6
386         self.invokeBlack([str(source_path), "--target-version", "py36"])
387         # but not on 3.7, because async/await is no longer an identifier
388         self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
389
390     @patch("black.dump_to_file", dump_to_stderr)
391     def test_python37(self) -> None:
392         source_path = (THIS_DIR / "data" / "python37.py").resolve()
393         source, expected = read_data("python37")
394         actual = fs(source)
395         self.assertFormatEqual(expected, actual)
396         major, minor = sys.version_info[:2]
397         if major > 3 or (major == 3 and minor >= 7):
398             black.assert_equivalent(source, actual)
399         black.assert_stable(source, actual, DEFAULT_MODE)
400         # ensure black can parse this when the target is 3.7
401         self.invokeBlack([str(source_path), "--target-version", "py37"])
402         # but not on 3.6, because we use async as a reserved keyword
403         self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
404
405     def test_tab_comment_indentation(self) -> None:
406         contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
407         contents_spc = "if 1:\n    if 2:\n        pass\n    # comment\n    pass\n"
408         self.assertFormatEqual(contents_spc, fs(contents_spc))
409         self.assertFormatEqual(contents_spc, fs(contents_tab))
410
411         contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
412         contents_spc = "if 1:\n    if 2:\n        pass\n        # comment\n    pass\n"
413         self.assertFormatEqual(contents_spc, fs(contents_spc))
414         self.assertFormatEqual(contents_spc, fs(contents_tab))
415
416         # mixed tabs and spaces (valid Python 2 code)
417         contents_tab = "if 1:\n        if 2:\n\t\tpass\n\t# comment\n        pass\n"
418         contents_spc = "if 1:\n    if 2:\n        pass\n    # comment\n    pass\n"
419         self.assertFormatEqual(contents_spc, fs(contents_spc))
420         self.assertFormatEqual(contents_spc, fs(contents_tab))
421
422         contents_tab = "if 1:\n        if 2:\n\t\tpass\n\t\t# comment\n        pass\n"
423         contents_spc = "if 1:\n    if 2:\n        pass\n        # comment\n    pass\n"
424         self.assertFormatEqual(contents_spc, fs(contents_spc))
425         self.assertFormatEqual(contents_spc, fs(contents_tab))
426
427     def test_report_verbose(self) -> None:
428         report = Report(verbose=True)
429         out_lines = []
430         err_lines = []
431
432         def out(msg: str, **kwargs: Any) -> None:
433             out_lines.append(msg)
434
435         def err(msg: str, **kwargs: Any) -> None:
436             err_lines.append(msg)
437
438         with patch("black.output._out", out), patch("black.output._err", err):
439             report.done(Path("f1"), black.Changed.NO)
440             self.assertEqual(len(out_lines), 1)
441             self.assertEqual(len(err_lines), 0)
442             self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
443             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
444             self.assertEqual(report.return_code, 0)
445             report.done(Path("f2"), black.Changed.YES)
446             self.assertEqual(len(out_lines), 2)
447             self.assertEqual(len(err_lines), 0)
448             self.assertEqual(out_lines[-1], "reformatted f2")
449             self.assertEqual(
450                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
451             )
452             report.done(Path("f3"), black.Changed.CACHED)
453             self.assertEqual(len(out_lines), 3)
454             self.assertEqual(len(err_lines), 0)
455             self.assertEqual(
456                 out_lines[-1], "f3 wasn't modified on disk since last run."
457             )
458             self.assertEqual(
459                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
460             )
461             self.assertEqual(report.return_code, 0)
462             report.check = True
463             self.assertEqual(report.return_code, 1)
464             report.check = False
465             report.failed(Path("e1"), "boom")
466             self.assertEqual(len(out_lines), 3)
467             self.assertEqual(len(err_lines), 1)
468             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
469             self.assertEqual(
470                 unstyle(str(report)),
471                 "1 file reformatted, 2 files left unchanged, 1 file failed to"
472                 " reformat.",
473             )
474             self.assertEqual(report.return_code, 123)
475             report.done(Path("f3"), black.Changed.YES)
476             self.assertEqual(len(out_lines), 4)
477             self.assertEqual(len(err_lines), 1)
478             self.assertEqual(out_lines[-1], "reformatted f3")
479             self.assertEqual(
480                 unstyle(str(report)),
481                 "2 files reformatted, 2 files left unchanged, 1 file failed to"
482                 " reformat.",
483             )
484             self.assertEqual(report.return_code, 123)
485             report.failed(Path("e2"), "boom")
486             self.assertEqual(len(out_lines), 4)
487             self.assertEqual(len(err_lines), 2)
488             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
489             self.assertEqual(
490                 unstyle(str(report)),
491                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
492                 " reformat.",
493             )
494             self.assertEqual(report.return_code, 123)
495             report.path_ignored(Path("wat"), "no match")
496             self.assertEqual(len(out_lines), 5)
497             self.assertEqual(len(err_lines), 2)
498             self.assertEqual(out_lines[-1], "wat ignored: no match")
499             self.assertEqual(
500                 unstyle(str(report)),
501                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
502                 " reformat.",
503             )
504             self.assertEqual(report.return_code, 123)
505             report.done(Path("f4"), black.Changed.NO)
506             self.assertEqual(len(out_lines), 6)
507             self.assertEqual(len(err_lines), 2)
508             self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
509             self.assertEqual(
510                 unstyle(str(report)),
511                 "2 files reformatted, 3 files left unchanged, 2 files failed to"
512                 " reformat.",
513             )
514             self.assertEqual(report.return_code, 123)
515             report.check = True
516             self.assertEqual(
517                 unstyle(str(report)),
518                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
519                 " would fail to reformat.",
520             )
521             report.check = False
522             report.diff = True
523             self.assertEqual(
524                 unstyle(str(report)),
525                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
526                 " would fail to reformat.",
527             )
528
529     def test_report_quiet(self) -> None:
530         report = Report(quiet=True)
531         out_lines = []
532         err_lines = []
533
534         def out(msg: str, **kwargs: Any) -> None:
535             out_lines.append(msg)
536
537         def err(msg: str, **kwargs: Any) -> None:
538             err_lines.append(msg)
539
540         with patch("black.output._out", out), patch("black.output._err", err):
541             report.done(Path("f1"), black.Changed.NO)
542             self.assertEqual(len(out_lines), 0)
543             self.assertEqual(len(err_lines), 0)
544             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
545             self.assertEqual(report.return_code, 0)
546             report.done(Path("f2"), black.Changed.YES)
547             self.assertEqual(len(out_lines), 0)
548             self.assertEqual(len(err_lines), 0)
549             self.assertEqual(
550                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
551             )
552             report.done(Path("f3"), black.Changed.CACHED)
553             self.assertEqual(len(out_lines), 0)
554             self.assertEqual(len(err_lines), 0)
555             self.assertEqual(
556                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
557             )
558             self.assertEqual(report.return_code, 0)
559             report.check = True
560             self.assertEqual(report.return_code, 1)
561             report.check = False
562             report.failed(Path("e1"), "boom")
563             self.assertEqual(len(out_lines), 0)
564             self.assertEqual(len(err_lines), 1)
565             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
566             self.assertEqual(
567                 unstyle(str(report)),
568                 "1 file reformatted, 2 files left unchanged, 1 file failed to"
569                 " reformat.",
570             )
571             self.assertEqual(report.return_code, 123)
572             report.done(Path("f3"), black.Changed.YES)
573             self.assertEqual(len(out_lines), 0)
574             self.assertEqual(len(err_lines), 1)
575             self.assertEqual(
576                 unstyle(str(report)),
577                 "2 files reformatted, 2 files left unchanged, 1 file failed to"
578                 " reformat.",
579             )
580             self.assertEqual(report.return_code, 123)
581             report.failed(Path("e2"), "boom")
582             self.assertEqual(len(out_lines), 0)
583             self.assertEqual(len(err_lines), 2)
584             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
585             self.assertEqual(
586                 unstyle(str(report)),
587                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
588                 " reformat.",
589             )
590             self.assertEqual(report.return_code, 123)
591             report.path_ignored(Path("wat"), "no match")
592             self.assertEqual(len(out_lines), 0)
593             self.assertEqual(len(err_lines), 2)
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.done(Path("f4"), black.Changed.NO)
601             self.assertEqual(len(out_lines), 0)
602             self.assertEqual(len(err_lines), 2)
603             self.assertEqual(
604                 unstyle(str(report)),
605                 "2 files reformatted, 3 files left unchanged, 2 files failed to"
606                 " reformat.",
607             )
608             self.assertEqual(report.return_code, 123)
609             report.check = True
610             self.assertEqual(
611                 unstyle(str(report)),
612                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
613                 " would fail to reformat.",
614             )
615             report.check = False
616             report.diff = True
617             self.assertEqual(
618                 unstyle(str(report)),
619                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
620                 " would fail to reformat.",
621             )
622
623     def test_report_normal(self) -> None:
624         report = black.Report()
625         out_lines = []
626         err_lines = []
627
628         def out(msg: str, **kwargs: Any) -> None:
629             out_lines.append(msg)
630
631         def err(msg: str, **kwargs: Any) -> None:
632             err_lines.append(msg)
633
634         with patch("black.output._out", out), patch("black.output._err", err):
635             report.done(Path("f1"), black.Changed.NO)
636             self.assertEqual(len(out_lines), 0)
637             self.assertEqual(len(err_lines), 0)
638             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
639             self.assertEqual(report.return_code, 0)
640             report.done(Path("f2"), black.Changed.YES)
641             self.assertEqual(len(out_lines), 1)
642             self.assertEqual(len(err_lines), 0)
643             self.assertEqual(out_lines[-1], "reformatted f2")
644             self.assertEqual(
645                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
646             )
647             report.done(Path("f3"), black.Changed.CACHED)
648             self.assertEqual(len(out_lines), 1)
649             self.assertEqual(len(err_lines), 0)
650             self.assertEqual(out_lines[-1], "reformatted f2")
651             self.assertEqual(
652                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
653             )
654             self.assertEqual(report.return_code, 0)
655             report.check = True
656             self.assertEqual(report.return_code, 1)
657             report.check = False
658             report.failed(Path("e1"), "boom")
659             self.assertEqual(len(out_lines), 1)
660             self.assertEqual(len(err_lines), 1)
661             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
662             self.assertEqual(
663                 unstyle(str(report)),
664                 "1 file reformatted, 2 files left unchanged, 1 file failed to"
665                 " reformat.",
666             )
667             self.assertEqual(report.return_code, 123)
668             report.done(Path("f3"), black.Changed.YES)
669             self.assertEqual(len(out_lines), 2)
670             self.assertEqual(len(err_lines), 1)
671             self.assertEqual(out_lines[-1], "reformatted f3")
672             self.assertEqual(
673                 unstyle(str(report)),
674                 "2 files reformatted, 2 files left unchanged, 1 file failed to"
675                 " reformat.",
676             )
677             self.assertEqual(report.return_code, 123)
678             report.failed(Path("e2"), "boom")
679             self.assertEqual(len(out_lines), 2)
680             self.assertEqual(len(err_lines), 2)
681             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
682             self.assertEqual(
683                 unstyle(str(report)),
684                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
685                 " reformat.",
686             )
687             self.assertEqual(report.return_code, 123)
688             report.path_ignored(Path("wat"), "no match")
689             self.assertEqual(len(out_lines), 2)
690             self.assertEqual(len(err_lines), 2)
691             self.assertEqual(
692                 unstyle(str(report)),
693                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
694                 " reformat.",
695             )
696             self.assertEqual(report.return_code, 123)
697             report.done(Path("f4"), black.Changed.NO)
698             self.assertEqual(len(out_lines), 2)
699             self.assertEqual(len(err_lines), 2)
700             self.assertEqual(
701                 unstyle(str(report)),
702                 "2 files reformatted, 3 files left unchanged, 2 files failed to"
703                 " reformat.",
704             )
705             self.assertEqual(report.return_code, 123)
706             report.check = True
707             self.assertEqual(
708                 unstyle(str(report)),
709                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
710                 " would fail to reformat.",
711             )
712             report.check = False
713             report.diff = True
714             self.assertEqual(
715                 unstyle(str(report)),
716                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
717                 " would fail to reformat.",
718             )
719
720     def test_lib2to3_parse(self) -> None:
721         with self.assertRaises(black.InvalidInput):
722             black.lib2to3_parse("invalid syntax")
723
724         straddling = "x + y"
725         black.lib2to3_parse(straddling)
726         black.lib2to3_parse(straddling, {TargetVersion.PY27})
727         black.lib2to3_parse(straddling, {TargetVersion.PY36})
728         black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
729
730         py2_only = "print x"
731         black.lib2to3_parse(py2_only)
732         black.lib2to3_parse(py2_only, {TargetVersion.PY27})
733         with self.assertRaises(black.InvalidInput):
734             black.lib2to3_parse(py2_only, {TargetVersion.PY36})
735         with self.assertRaises(black.InvalidInput):
736             black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
737
738         py3_only = "exec(x, end=y)"
739         black.lib2to3_parse(py3_only)
740         with self.assertRaises(black.InvalidInput):
741             black.lib2to3_parse(py3_only, {TargetVersion.PY27})
742         black.lib2to3_parse(py3_only, {TargetVersion.PY36})
743         black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
744
745     def test_get_features_used_decorator(self) -> None:
746         # Test the feature detection of new decorator syntax
747         # since this makes some test cases of test_get_features_used()
748         # fails if it fails, this is tested first so that a useful case
749         # is identified
750         simples, relaxed = read_data("decorators")
751         # skip explanation comments at the top of the file
752         for simple_test in simples.split("##")[1:]:
753             node = black.lib2to3_parse(simple_test)
754             decorator = str(node.children[0].children[0]).strip()
755             self.assertNotIn(
756                 Feature.RELAXED_DECORATORS,
757                 black.get_features_used(node),
758                 msg=(
759                     f"decorator '{decorator}' follows python<=3.8 syntax"
760                     "but is detected as 3.9+"
761                     # f"The full node is\n{node!r}"
762                 ),
763             )
764         # skip the '# output' comment at the top of the output part
765         for relaxed_test in relaxed.split("##")[1:]:
766             node = black.lib2to3_parse(relaxed_test)
767             decorator = str(node.children[0].children[0]).strip()
768             self.assertIn(
769                 Feature.RELAXED_DECORATORS,
770                 black.get_features_used(node),
771                 msg=(
772                     f"decorator '{decorator}' uses python3.9+ syntax"
773                     "but is detected as python<=3.8"
774                     # f"The full node is\n{node!r}"
775                 ),
776             )
777
778     def test_get_features_used(self) -> None:
779         node = black.lib2to3_parse("def f(*, arg): ...\n")
780         self.assertEqual(black.get_features_used(node), set())
781         node = black.lib2to3_parse("def f(*, arg,): ...\n")
782         self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
783         node = black.lib2to3_parse("f(*arg,)\n")
784         self.assertEqual(
785             black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
786         )
787         node = black.lib2to3_parse("def f(*, arg): f'string'\n")
788         self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
789         node = black.lib2to3_parse("123_456\n")
790         self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
791         node = black.lib2to3_parse("123456\n")
792         self.assertEqual(black.get_features_used(node), set())
793         source, expected = read_data("function")
794         node = black.lib2to3_parse(source)
795         expected_features = {
796             Feature.TRAILING_COMMA_IN_CALL,
797             Feature.TRAILING_COMMA_IN_DEF,
798             Feature.F_STRINGS,
799         }
800         self.assertEqual(black.get_features_used(node), expected_features)
801         node = black.lib2to3_parse(expected)
802         self.assertEqual(black.get_features_used(node), expected_features)
803         source, expected = read_data("expression")
804         node = black.lib2to3_parse(source)
805         self.assertEqual(black.get_features_used(node), set())
806         node = black.lib2to3_parse(expected)
807         self.assertEqual(black.get_features_used(node), set())
808         node = black.lib2to3_parse("lambda a, /, b: ...")
809         self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
810         node = black.lib2to3_parse("def fn(a, /, b): ...")
811         self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
812
813     def test_get_future_imports(self) -> None:
814         node = black.lib2to3_parse("\n")
815         self.assertEqual(set(), black.get_future_imports(node))
816         node = black.lib2to3_parse("from __future__ import black\n")
817         self.assertEqual({"black"}, black.get_future_imports(node))
818         node = black.lib2to3_parse("from __future__ import multiple, imports\n")
819         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
820         node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
821         self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
822         node = black.lib2to3_parse(
823             "from __future__ import multiple\nfrom __future__ import imports\n"
824         )
825         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
826         node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
827         self.assertEqual({"black"}, black.get_future_imports(node))
828         node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
829         self.assertEqual({"black"}, black.get_future_imports(node))
830         node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
831         self.assertEqual(set(), black.get_future_imports(node))
832         node = black.lib2to3_parse("from some.module import black\n")
833         self.assertEqual(set(), black.get_future_imports(node))
834         node = black.lib2to3_parse(
835             "from __future__ import unicode_literals as _unicode_literals"
836         )
837         self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
838         node = black.lib2to3_parse(
839             "from __future__ import unicode_literals as _lol, print"
840         )
841         self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
842
843     def test_debug_visitor(self) -> None:
844         source, _ = read_data("debug_visitor.py")
845         expected, _ = read_data("debug_visitor.out")
846         out_lines = []
847         err_lines = []
848
849         def out(msg: str, **kwargs: Any) -> None:
850             out_lines.append(msg)
851
852         def err(msg: str, **kwargs: Any) -> None:
853             err_lines.append(msg)
854
855         with patch("black.debug.out", out):
856             DebugVisitor.show(source)
857         actual = "\n".join(out_lines) + "\n"
858         log_name = ""
859         if expected != actual:
860             log_name = black.dump_to_file(*out_lines)
861         self.assertEqual(
862             expected,
863             actual,
864             f"AST print out is different. Actual version dumped to {log_name}",
865         )
866
867     def test_format_file_contents(self) -> None:
868         empty = ""
869         mode = DEFAULT_MODE
870         with self.assertRaises(black.NothingChanged):
871             black.format_file_contents(empty, mode=mode, fast=False)
872         just_nl = "\n"
873         with self.assertRaises(black.NothingChanged):
874             black.format_file_contents(just_nl, mode=mode, fast=False)
875         same = "j = [1, 2, 3]\n"
876         with self.assertRaises(black.NothingChanged):
877             black.format_file_contents(same, mode=mode, fast=False)
878         different = "j = [1,2,3]"
879         expected = same
880         actual = black.format_file_contents(different, mode=mode, fast=False)
881         self.assertEqual(expected, actual)
882         invalid = "return if you can"
883         with self.assertRaises(black.InvalidInput) as e:
884             black.format_file_contents(invalid, mode=mode, fast=False)
885         self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
886
887     def test_endmarker(self) -> None:
888         n = black.lib2to3_parse("\n")
889         self.assertEqual(n.type, black.syms.file_input)
890         self.assertEqual(len(n.children), 1)
891         self.assertEqual(n.children[0].type, black.token.ENDMARKER)
892
893     @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
894     def test_assertFormatEqual(self) -> None:
895         out_lines = []
896         err_lines = []
897
898         def out(msg: str, **kwargs: Any) -> None:
899             out_lines.append(msg)
900
901         def err(msg: str, **kwargs: Any) -> None:
902             err_lines.append(msg)
903
904         with patch("black.output._out", out), patch("black.output._err", err):
905             with self.assertRaises(AssertionError):
906                 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
907
908         out_str = "".join(out_lines)
909         self.assertTrue("Expected tree:" in out_str)
910         self.assertTrue("Actual tree:" in out_str)
911         self.assertEqual("".join(err_lines), "")
912
913     @event_loop()
914     @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
915     def test_works_in_mono_process_only_environment(self) -> None:
916         with cache_dir() as workspace:
917             for f in [
918                 (workspace / "one.py").resolve(),
919                 (workspace / "two.py").resolve(),
920             ]:
921                 f.write_text('print("hello")\n')
922             self.invokeBlack([str(workspace)])
923
924     @event_loop()
925     def test_check_diff_use_together(self) -> None:
926         with cache_dir():
927             # Files which will be reformatted.
928             src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
929             self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
930             # Files which will not be reformatted.
931             src2 = (THIS_DIR / "data" / "composition.py").resolve()
932             self.invokeBlack([str(src2), "--diff", "--check"])
933             # Multi file command.
934             self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
935
936     def test_no_files(self) -> None:
937         with cache_dir():
938             # Without an argument, black exits with error code 0.
939             self.invokeBlack([])
940
941     def test_broken_symlink(self) -> None:
942         with cache_dir() as workspace:
943             symlink = workspace / "broken_link.py"
944             try:
945                 symlink.symlink_to("nonexistent.py")
946             except OSError as e:
947                 self.skipTest(f"Can't create symlinks: {e}")
948             self.invokeBlack([str(workspace.resolve())])
949
950     def test_single_file_force_pyi(self) -> None:
951         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
952         contents, expected = read_data("force_pyi")
953         with cache_dir() as workspace:
954             path = (workspace / "file.py").resolve()
955             with open(path, "w") as fh:
956                 fh.write(contents)
957             self.invokeBlack([str(path), "--pyi"])
958             with open(path, "r") as fh:
959                 actual = fh.read()
960             # verify cache with --pyi is separate
961             pyi_cache = black.read_cache(pyi_mode)
962             self.assertIn(str(path), pyi_cache)
963             normal_cache = black.read_cache(DEFAULT_MODE)
964             self.assertNotIn(str(path), normal_cache)
965         self.assertFormatEqual(expected, actual)
966         black.assert_equivalent(contents, actual)
967         black.assert_stable(contents, actual, pyi_mode)
968
969     @event_loop()
970     def test_multi_file_force_pyi(self) -> None:
971         reg_mode = DEFAULT_MODE
972         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
973         contents, expected = read_data("force_pyi")
974         with cache_dir() as workspace:
975             paths = [
976                 (workspace / "file1.py").resolve(),
977                 (workspace / "file2.py").resolve(),
978             ]
979             for path in paths:
980                 with open(path, "w") as fh:
981                     fh.write(contents)
982             self.invokeBlack([str(p) for p in paths] + ["--pyi"])
983             for path in paths:
984                 with open(path, "r") as fh:
985                     actual = fh.read()
986                 self.assertEqual(actual, expected)
987             # verify cache with --pyi is separate
988             pyi_cache = black.read_cache(pyi_mode)
989             normal_cache = black.read_cache(reg_mode)
990             for path in paths:
991                 self.assertIn(str(path), pyi_cache)
992                 self.assertNotIn(str(path), normal_cache)
993
994     def test_pipe_force_pyi(self) -> None:
995         source, expected = read_data("force_pyi")
996         result = CliRunner().invoke(
997             black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
998         )
999         self.assertEqual(result.exit_code, 0)
1000         actual = result.output
1001         self.assertFormatEqual(actual, expected)
1002
1003     def test_single_file_force_py36(self) -> None:
1004         reg_mode = DEFAULT_MODE
1005         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1006         source, expected = read_data("force_py36")
1007         with cache_dir() as workspace:
1008             path = (workspace / "file.py").resolve()
1009             with open(path, "w") as fh:
1010                 fh.write(source)
1011             self.invokeBlack([str(path), *PY36_ARGS])
1012             with open(path, "r") as fh:
1013                 actual = fh.read()
1014             # verify cache with --target-version is separate
1015             py36_cache = black.read_cache(py36_mode)
1016             self.assertIn(str(path), py36_cache)
1017             normal_cache = black.read_cache(reg_mode)
1018             self.assertNotIn(str(path), normal_cache)
1019         self.assertEqual(actual, expected)
1020
1021     @event_loop()
1022     def test_multi_file_force_py36(self) -> None:
1023         reg_mode = DEFAULT_MODE
1024         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1025         source, expected = read_data("force_py36")
1026         with cache_dir() as workspace:
1027             paths = [
1028                 (workspace / "file1.py").resolve(),
1029                 (workspace / "file2.py").resolve(),
1030             ]
1031             for path in paths:
1032                 with open(path, "w") as fh:
1033                     fh.write(source)
1034             self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1035             for path in paths:
1036                 with open(path, "r") as fh:
1037                     actual = fh.read()
1038                 self.assertEqual(actual, expected)
1039             # verify cache with --target-version is separate
1040             pyi_cache = black.read_cache(py36_mode)
1041             normal_cache = black.read_cache(reg_mode)
1042             for path in paths:
1043                 self.assertIn(str(path), pyi_cache)
1044                 self.assertNotIn(str(path), normal_cache)
1045
1046     def test_pipe_force_py36(self) -> None:
1047         source, expected = read_data("force_py36")
1048         result = CliRunner().invoke(
1049             black.main,
1050             ["-", "-q", "--target-version=py36"],
1051             input=BytesIO(source.encode("utf8")),
1052         )
1053         self.assertEqual(result.exit_code, 0)
1054         actual = result.output
1055         self.assertFormatEqual(actual, expected)
1056
1057     def test_reformat_one_with_stdin(self) -> None:
1058         with patch(
1059             "black.format_stdin_to_stdout",
1060             return_value=lambda *args, **kwargs: black.Changed.YES,
1061         ) as fsts:
1062             report = MagicMock()
1063             path = Path("-")
1064             black.reformat_one(
1065                 path,
1066                 fast=True,
1067                 write_back=black.WriteBack.YES,
1068                 mode=DEFAULT_MODE,
1069                 report=report,
1070             )
1071             fsts.assert_called_once()
1072             report.done.assert_called_with(path, black.Changed.YES)
1073
1074     def test_reformat_one_with_stdin_filename(self) -> None:
1075         with patch(
1076             "black.format_stdin_to_stdout",
1077             return_value=lambda *args, **kwargs: black.Changed.YES,
1078         ) as fsts:
1079             report = MagicMock()
1080             p = "foo.py"
1081             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1082             expected = Path(p)
1083             black.reformat_one(
1084                 path,
1085                 fast=True,
1086                 write_back=black.WriteBack.YES,
1087                 mode=DEFAULT_MODE,
1088                 report=report,
1089             )
1090             fsts.assert_called_once_with(
1091                 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1092             )
1093             # __BLACK_STDIN_FILENAME__ should have been stripped
1094             report.done.assert_called_with(expected, black.Changed.YES)
1095
1096     def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1097         with patch(
1098             "black.format_stdin_to_stdout",
1099             return_value=lambda *args, **kwargs: black.Changed.YES,
1100         ) as fsts:
1101             report = MagicMock()
1102             p = "foo.pyi"
1103             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1104             expected = Path(p)
1105             black.reformat_one(
1106                 path,
1107                 fast=True,
1108                 write_back=black.WriteBack.YES,
1109                 mode=DEFAULT_MODE,
1110                 report=report,
1111             )
1112             fsts.assert_called_once_with(
1113                 fast=True,
1114                 write_back=black.WriteBack.YES,
1115                 mode=replace(DEFAULT_MODE, is_pyi=True),
1116             )
1117             # __BLACK_STDIN_FILENAME__ should have been stripped
1118             report.done.assert_called_with(expected, black.Changed.YES)
1119
1120     def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1121         with patch(
1122             "black.format_stdin_to_stdout",
1123             return_value=lambda *args, **kwargs: black.Changed.YES,
1124         ) as fsts:
1125             report = MagicMock()
1126             p = "foo.ipynb"
1127             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1128             expected = Path(p)
1129             black.reformat_one(
1130                 path,
1131                 fast=True,
1132                 write_back=black.WriteBack.YES,
1133                 mode=DEFAULT_MODE,
1134                 report=report,
1135             )
1136             fsts.assert_called_once_with(
1137                 fast=True,
1138                 write_back=black.WriteBack.YES,
1139                 mode=replace(DEFAULT_MODE, is_ipynb=True),
1140             )
1141             # __BLACK_STDIN_FILENAME__ should have been stripped
1142             report.done.assert_called_with(expected, black.Changed.YES)
1143
1144     def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1145         with patch(
1146             "black.format_stdin_to_stdout",
1147             return_value=lambda *args, **kwargs: black.Changed.YES,
1148         ) as fsts:
1149             report = MagicMock()
1150             # Even with an existing file, since we are forcing stdin, black
1151             # should output to stdout and not modify the file inplace
1152             p = Path(str(THIS_DIR / "data/collections.py"))
1153             # Make sure is_file actually returns True
1154             self.assertTrue(p.is_file())
1155             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1156             expected = Path(p)
1157             black.reformat_one(
1158                 path,
1159                 fast=True,
1160                 write_back=black.WriteBack.YES,
1161                 mode=DEFAULT_MODE,
1162                 report=report,
1163             )
1164             fsts.assert_called_once()
1165             # __BLACK_STDIN_FILENAME__ should have been stripped
1166             report.done.assert_called_with(expected, black.Changed.YES)
1167
1168     def test_reformat_one_with_stdin_empty(self) -> None:
1169         output = io.StringIO()
1170         with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1171             try:
1172                 black.format_stdin_to_stdout(
1173                     fast=True,
1174                     content="",
1175                     write_back=black.WriteBack.YES,
1176                     mode=DEFAULT_MODE,
1177                 )
1178             except io.UnsupportedOperation:
1179                 pass  # StringIO does not support detach
1180             assert output.getvalue() == ""
1181
1182     def test_invalid_cli_regex(self) -> None:
1183         for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1184             self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1185
1186     def test_required_version_matches_version(self) -> None:
1187         self.invokeBlack(
1188             ["--required-version", black.__version__], exit_code=0, ignore_config=True
1189         )
1190
1191     def test_required_version_does_not_match_version(self) -> None:
1192         self.invokeBlack(
1193             ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1194         )
1195
1196     def test_preserves_line_endings(self) -> None:
1197         with TemporaryDirectory() as workspace:
1198             test_file = Path(workspace) / "test.py"
1199             for nl in ["\n", "\r\n"]:
1200                 contents = nl.join(["def f(  ):", "    pass"])
1201                 test_file.write_bytes(contents.encode())
1202                 ff(test_file, write_back=black.WriteBack.YES)
1203                 updated_contents: bytes = test_file.read_bytes()
1204                 self.assertIn(nl.encode(), updated_contents)
1205                 if nl == "\n":
1206                     self.assertNotIn(b"\r\n", updated_contents)
1207
1208     def test_preserves_line_endings_via_stdin(self) -> None:
1209         for nl in ["\n", "\r\n"]:
1210             contents = nl.join(["def f(  ):", "    pass"])
1211             runner = BlackRunner()
1212             result = runner.invoke(
1213                 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1214             )
1215             self.assertEqual(result.exit_code, 0)
1216             output = result.stdout_bytes
1217             self.assertIn(nl.encode("utf8"), output)
1218             if nl == "\n":
1219                 self.assertNotIn(b"\r\n", output)
1220
1221     def test_assert_equivalent_different_asts(self) -> None:
1222         with self.assertRaises(AssertionError):
1223             black.assert_equivalent("{}", "None")
1224
1225     def test_shhh_click(self) -> None:
1226         try:
1227             from click import _unicodefun
1228         except ModuleNotFoundError:
1229             self.skipTest("Incompatible Click version")
1230         if not hasattr(_unicodefun, "_verify_python3_env"):
1231             self.skipTest("Incompatible Click version")
1232         # First, let's see if Click is crashing with a preferred ASCII charset.
1233         with patch("locale.getpreferredencoding") as gpe:
1234             gpe.return_value = "ASCII"
1235             with self.assertRaises(RuntimeError):
1236                 _unicodefun._verify_python3_env()  # type: ignore
1237         # Now, let's silence Click...
1238         black.patch_click()
1239         # ...and confirm it's silent.
1240         with patch("locale.getpreferredencoding") as gpe:
1241             gpe.return_value = "ASCII"
1242             try:
1243                 _unicodefun._verify_python3_env()  # type: ignore
1244             except RuntimeError as re:
1245                 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1246
1247     def test_root_logger_not_used_directly(self) -> None:
1248         def fail(*args: Any, **kwargs: Any) -> None:
1249             self.fail("Record created with root logger")
1250
1251         with patch.multiple(
1252             logging.root,
1253             debug=fail,
1254             info=fail,
1255             warning=fail,
1256             error=fail,
1257             critical=fail,
1258             log=fail,
1259         ):
1260             ff(THIS_DIR / "util.py")
1261
1262     def test_invalid_config_return_code(self) -> None:
1263         tmp_file = Path(black.dump_to_file())
1264         try:
1265             tmp_config = Path(black.dump_to_file())
1266             tmp_config.unlink()
1267             args = ["--config", str(tmp_config), str(tmp_file)]
1268             self.invokeBlack(args, exit_code=2, ignore_config=False)
1269         finally:
1270             tmp_file.unlink()
1271
1272     def test_parse_pyproject_toml(self) -> None:
1273         test_toml_file = THIS_DIR / "test.toml"
1274         config = black.parse_pyproject_toml(str(test_toml_file))
1275         self.assertEqual(config["verbose"], 1)
1276         self.assertEqual(config["check"], "no")
1277         self.assertEqual(config["diff"], "y")
1278         self.assertEqual(config["color"], True)
1279         self.assertEqual(config["line_length"], 79)
1280         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1281         self.assertEqual(config["exclude"], r"\.pyi?$")
1282         self.assertEqual(config["include"], r"\.py?$")
1283
1284     def test_read_pyproject_toml(self) -> None:
1285         test_toml_file = THIS_DIR / "test.toml"
1286         fake_ctx = FakeContext()
1287         black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1288         config = fake_ctx.default_map
1289         self.assertEqual(config["verbose"], "1")
1290         self.assertEqual(config["check"], "no")
1291         self.assertEqual(config["diff"], "y")
1292         self.assertEqual(config["color"], "True")
1293         self.assertEqual(config["line_length"], "79")
1294         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1295         self.assertEqual(config["exclude"], r"\.pyi?$")
1296         self.assertEqual(config["include"], r"\.py?$")
1297
1298     def test_find_project_root(self) -> None:
1299         with TemporaryDirectory() as workspace:
1300             root = Path(workspace)
1301             test_dir = root / "test"
1302             test_dir.mkdir()
1303
1304             src_dir = root / "src"
1305             src_dir.mkdir()
1306
1307             root_pyproject = root / "pyproject.toml"
1308             root_pyproject.touch()
1309             src_pyproject = src_dir / "pyproject.toml"
1310             src_pyproject.touch()
1311             src_python = src_dir / "foo.py"
1312             src_python.touch()
1313
1314             self.assertEqual(
1315                 black.find_project_root((src_dir, test_dir)), root.resolve()
1316             )
1317             self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1318             self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1319
1320     @patch(
1321         "black.files.find_user_pyproject_toml",
1322         black.files.find_user_pyproject_toml.__wrapped__,
1323     )
1324     def test_find_user_pyproject_toml_linux(self) -> None:
1325         if system() == "Windows":
1326             return
1327
1328         # Test if XDG_CONFIG_HOME is checked
1329         with TemporaryDirectory() as workspace:
1330             tmp_user_config = Path(workspace) / "black"
1331             with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1332                 self.assertEqual(
1333                     black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1334                 )
1335
1336         # Test fallback for XDG_CONFIG_HOME
1337         with patch.dict("os.environ"):
1338             os.environ.pop("XDG_CONFIG_HOME", None)
1339             fallback_user_config = Path("~/.config").expanduser() / "black"
1340             self.assertEqual(
1341                 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1342             )
1343
1344     def test_find_user_pyproject_toml_windows(self) -> None:
1345         if system() != "Windows":
1346             return
1347
1348         user_config_path = Path.home() / ".black"
1349         self.assertEqual(
1350             black.files.find_user_pyproject_toml(), user_config_path.resolve()
1351         )
1352
1353     def test_bpo_33660_workaround(self) -> None:
1354         if system() == "Windows":
1355             return
1356
1357         # https://bugs.python.org/issue33660
1358         root = Path("/")
1359         with change_directory(root):
1360             path = Path("workspace") / "project"
1361             report = black.Report(verbose=True)
1362             normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1363             self.assertEqual(normalized_path, "workspace/project")
1364
1365     def test_newline_comment_interaction(self) -> None:
1366         source = "class A:\\\r\n# type: ignore\n pass\n"
1367         output = black.format_str(source, mode=DEFAULT_MODE)
1368         black.assert_stable(source, output, mode=DEFAULT_MODE)
1369
1370     def test_bpo_2142_workaround(self) -> None:
1371
1372         # https://bugs.python.org/issue2142
1373
1374         source, _ = read_data("missing_final_newline.py")
1375         # read_data adds a trailing newline
1376         source = source.rstrip()
1377         expected, _ = read_data("missing_final_newline.diff")
1378         tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1379         diff_header = re.compile(
1380             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1381             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1382         )
1383         try:
1384             result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1385             self.assertEqual(result.exit_code, 0)
1386         finally:
1387             os.unlink(tmp_file)
1388         actual = result.output
1389         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1390         self.assertEqual(actual, expected)
1391
1392     @pytest.mark.python2
1393     def test_docstring_reformat_for_py27(self) -> None:
1394         """
1395         Check that stripping trailing whitespace from Python 2 docstrings
1396         doesn't trigger a "not equivalent to source" error
1397         """
1398         source = (
1399             b'def foo():\r\n    """Testing\r\n    Testing """\r\n    print "Foo"\r\n'
1400         )
1401         expected = 'def foo():\n    """Testing\n    Testing"""\n    print "Foo"\n'
1402
1403         result = CliRunner().invoke(
1404             black.main,
1405             ["-", "-q", "--target-version=py27"],
1406             input=BytesIO(source),
1407         )
1408
1409         self.assertEqual(result.exit_code, 0)
1410         actual = result.output
1411         self.assertFormatEqual(actual, expected)
1412
1413     @staticmethod
1414     def compare_results(
1415         result: click.testing.Result, expected_value: str, expected_exit_code: int
1416     ) -> None:
1417         """Helper method to test the value and exit code of a click Result."""
1418         assert (
1419             result.output == expected_value
1420         ), "The output did not match the expected value."
1421         assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1422
1423     def test_code_option(self) -> None:
1424         """Test the code option with no changes."""
1425         code = 'print("Hello world")\n'
1426         args = ["--code", code]
1427         result = CliRunner().invoke(black.main, args)
1428
1429         self.compare_results(result, code, 0)
1430
1431     def test_code_option_changed(self) -> None:
1432         """Test the code option when changes are required."""
1433         code = "print('hello world')"
1434         formatted = black.format_str(code, mode=DEFAULT_MODE)
1435
1436         args = ["--code", code]
1437         result = CliRunner().invoke(black.main, args)
1438
1439         self.compare_results(result, formatted, 0)
1440
1441     def test_code_option_check(self) -> None:
1442         """Test the code option when check is passed."""
1443         args = ["--check", "--code", 'print("Hello world")\n']
1444         result = CliRunner().invoke(black.main, args)
1445         self.compare_results(result, "", 0)
1446
1447     def test_code_option_check_changed(self) -> None:
1448         """Test the code option when changes are required, and check is passed."""
1449         args = ["--check", "--code", "print('hello world')"]
1450         result = CliRunner().invoke(black.main, args)
1451         self.compare_results(result, "", 1)
1452
1453     def test_code_option_diff(self) -> None:
1454         """Test the code option when diff is passed."""
1455         code = "print('hello world')"
1456         formatted = black.format_str(code, mode=DEFAULT_MODE)
1457         result_diff = diff(code, formatted, "STDIN", "STDOUT")
1458
1459         args = ["--diff", "--code", code]
1460         result = CliRunner().invoke(black.main, args)
1461
1462         # Remove time from diff
1463         output = DIFF_TIME.sub("", result.output)
1464
1465         assert output == result_diff, "The output did not match the expected value."
1466         assert result.exit_code == 0, "The exit code is incorrect."
1467
1468     def test_code_option_color_diff(self) -> None:
1469         """Test the code option when color and diff are passed."""
1470         code = "print('hello world')"
1471         formatted = black.format_str(code, mode=DEFAULT_MODE)
1472
1473         result_diff = diff(code, formatted, "STDIN", "STDOUT")
1474         result_diff = color_diff(result_diff)
1475
1476         args = ["--diff", "--color", "--code", code]
1477         result = CliRunner().invoke(black.main, args)
1478
1479         # Remove time from diff
1480         output = DIFF_TIME.sub("", result.output)
1481
1482         assert output == result_diff, "The output did not match the expected value."
1483         assert result.exit_code == 0, "The exit code is incorrect."
1484
1485     def test_code_option_safe(self) -> None:
1486         """Test that the code option throws an error when the sanity checks fail."""
1487         # Patch black.assert_equivalent to ensure the sanity checks fail
1488         with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1489             code = 'print("Hello world")'
1490             error_msg = f"{code}\nerror: cannot format <string>: \n"
1491
1492             args = ["--safe", "--code", code]
1493             result = CliRunner().invoke(black.main, args)
1494
1495             self.compare_results(result, error_msg, 123)
1496
1497     def test_code_option_fast(self) -> None:
1498         """Test that the code option ignores errors when the sanity checks fail."""
1499         # Patch black.assert_equivalent to ensure the sanity checks fail
1500         with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1501             code = 'print("Hello world")'
1502             formatted = black.format_str(code, mode=DEFAULT_MODE)
1503
1504             args = ["--fast", "--code", code]
1505             result = CliRunner().invoke(black.main, args)
1506
1507             self.compare_results(result, formatted, 0)
1508
1509     def test_code_option_config(self) -> None:
1510         """
1511         Test that the code option finds the pyproject.toml in the current directory.
1512         """
1513         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1514             args = ["--code", "print"]
1515             CliRunner().invoke(black.main, args)
1516
1517             pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve()
1518             assert (
1519                 len(parse.mock_calls) >= 1
1520             ), "Expected config parse to be called with the current directory."
1521
1522             _, call_args, _ = parse.mock_calls[0]
1523             assert (
1524                 call_args[0].lower() == str(pyproject_path).lower()
1525             ), "Incorrect config loaded."
1526
1527     def test_code_option_parent_config(self) -> None:
1528         """
1529         Test that the code option finds the pyproject.toml in the parent directory.
1530         """
1531         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1532             with change_directory(Path("tests")):
1533                 args = ["--code", "print"]
1534                 CliRunner().invoke(black.main, args)
1535
1536                 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1537                 assert (
1538                     len(parse.mock_calls) >= 1
1539                 ), "Expected config parse to be called with the current directory."
1540
1541                 _, call_args, _ = parse.mock_calls[0]
1542                 assert (
1543                     call_args[0].lower() == str(pyproject_path).lower()
1544                 ), "Incorrect config loaded."
1545
1546
1547 class TestCaching:
1548     def test_cache_broken_file(self) -> None:
1549         mode = DEFAULT_MODE
1550         with cache_dir() as workspace:
1551             cache_file = get_cache_file(mode)
1552             cache_file.write_text("this is not a pickle")
1553             assert black.read_cache(mode) == {}
1554             src = (workspace / "test.py").resolve()
1555             src.write_text("print('hello')")
1556             invokeBlack([str(src)])
1557             cache = black.read_cache(mode)
1558             assert str(src) in cache
1559
1560     def test_cache_single_file_already_cached(self) -> None:
1561         mode = DEFAULT_MODE
1562         with cache_dir() as workspace:
1563             src = (workspace / "test.py").resolve()
1564             src.write_text("print('hello')")
1565             black.write_cache({}, [src], mode)
1566             invokeBlack([str(src)])
1567             assert src.read_text() == "print('hello')"
1568
1569     @event_loop()
1570     def test_cache_multiple_files(self) -> None:
1571         mode = DEFAULT_MODE
1572         with cache_dir() as workspace, patch(
1573             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1574         ):
1575             one = (workspace / "one.py").resolve()
1576             with one.open("w") as fobj:
1577                 fobj.write("print('hello')")
1578             two = (workspace / "two.py").resolve()
1579             with two.open("w") as fobj:
1580                 fobj.write("print('hello')")
1581             black.write_cache({}, [one], mode)
1582             invokeBlack([str(workspace)])
1583             with one.open("r") as fobj:
1584                 assert fobj.read() == "print('hello')"
1585             with two.open("r") as fobj:
1586                 assert fobj.read() == 'print("hello")\n'
1587             cache = black.read_cache(mode)
1588             assert str(one) in cache
1589             assert str(two) in cache
1590
1591     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1592     def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1593         mode = DEFAULT_MODE
1594         with cache_dir() as workspace:
1595             src = (workspace / "test.py").resolve()
1596             with src.open("w") as fobj:
1597                 fobj.write("print('hello')")
1598             with patch("black.read_cache") as read_cache, patch(
1599                 "black.write_cache"
1600             ) as write_cache:
1601                 cmd = [str(src), "--diff"]
1602                 if color:
1603                     cmd.append("--color")
1604                 invokeBlack(cmd)
1605                 cache_file = get_cache_file(mode)
1606                 assert cache_file.exists() is False
1607                 write_cache.assert_not_called()
1608                 read_cache.assert_not_called()
1609
1610     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1611     @event_loop()
1612     def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1613         with cache_dir() as workspace:
1614             for tag in range(0, 4):
1615                 src = (workspace / f"test{tag}.py").resolve()
1616                 with src.open("w") as fobj:
1617                     fobj.write("print('hello')")
1618             with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1619                 cmd = ["--diff", str(workspace)]
1620                 if color:
1621                     cmd.append("--color")
1622                 invokeBlack(cmd, exit_code=0)
1623                 # this isn't quite doing what we want, but if it _isn't_
1624                 # called then we cannot be using the lock it provides
1625                 mgr.assert_called()
1626
1627     def test_no_cache_when_stdin(self) -> None:
1628         mode = DEFAULT_MODE
1629         with cache_dir():
1630             result = CliRunner().invoke(
1631                 black.main, ["-"], input=BytesIO(b"print('hello')")
1632             )
1633             assert not result.exit_code
1634             cache_file = get_cache_file(mode)
1635             assert not cache_file.exists()
1636
1637     def test_read_cache_no_cachefile(self) -> None:
1638         mode = DEFAULT_MODE
1639         with cache_dir():
1640             assert black.read_cache(mode) == {}
1641
1642     def test_write_cache_read_cache(self) -> None:
1643         mode = DEFAULT_MODE
1644         with cache_dir() as workspace:
1645             src = (workspace / "test.py").resolve()
1646             src.touch()
1647             black.write_cache({}, [src], mode)
1648             cache = black.read_cache(mode)
1649             assert str(src) in cache
1650             assert cache[str(src)] == black.get_cache_info(src)
1651
1652     def test_filter_cached(self) -> None:
1653         with TemporaryDirectory() as workspace:
1654             path = Path(workspace)
1655             uncached = (path / "uncached").resolve()
1656             cached = (path / "cached").resolve()
1657             cached_but_changed = (path / "changed").resolve()
1658             uncached.touch()
1659             cached.touch()
1660             cached_but_changed.touch()
1661             cache = {
1662                 str(cached): black.get_cache_info(cached),
1663                 str(cached_but_changed): (0.0, 0),
1664             }
1665             todo, done = black.filter_cached(
1666                 cache, {uncached, cached, cached_but_changed}
1667             )
1668             assert todo == {uncached, cached_but_changed}
1669             assert done == {cached}
1670
1671     def test_write_cache_creates_directory_if_needed(self) -> None:
1672         mode = DEFAULT_MODE
1673         with cache_dir(exists=False) as workspace:
1674             assert not workspace.exists()
1675             black.write_cache({}, [], mode)
1676             assert workspace.exists()
1677
1678     @event_loop()
1679     def test_failed_formatting_does_not_get_cached(self) -> None:
1680         mode = DEFAULT_MODE
1681         with cache_dir() as workspace, patch(
1682             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1683         ):
1684             failing = (workspace / "failing.py").resolve()
1685             with failing.open("w") as fobj:
1686                 fobj.write("not actually python")
1687             clean = (workspace / "clean.py").resolve()
1688             with clean.open("w") as fobj:
1689                 fobj.write('print("hello")\n')
1690             invokeBlack([str(workspace)], exit_code=123)
1691             cache = black.read_cache(mode)
1692             assert str(failing) not in cache
1693             assert str(clean) in cache
1694
1695     def test_write_cache_write_fail(self) -> None:
1696         mode = DEFAULT_MODE
1697         with cache_dir(), patch.object(Path, "open") as mock:
1698             mock.side_effect = OSError
1699             black.write_cache({}, [], mode)
1700
1701     def test_read_cache_line_lengths(self) -> None:
1702         mode = DEFAULT_MODE
1703         short_mode = replace(DEFAULT_MODE, line_length=1)
1704         with cache_dir() as workspace:
1705             path = (workspace / "file.py").resolve()
1706             path.touch()
1707             black.write_cache({}, [path], mode)
1708             one = black.read_cache(mode)
1709             assert str(path) in one
1710             two = black.read_cache(short_mode)
1711             assert str(path) not in two
1712
1713
1714 def assert_collected_sources(
1715     src: Sequence[Union[str, Path]],
1716     expected: Sequence[Union[str, Path]],
1717     *,
1718     exclude: Optional[str] = None,
1719     include: Optional[str] = None,
1720     extend_exclude: Optional[str] = None,
1721     force_exclude: Optional[str] = None,
1722     stdin_filename: Optional[str] = None,
1723 ) -> None:
1724     gs_src = tuple(str(Path(s)) for s in src)
1725     gs_expected = [Path(s) for s in expected]
1726     gs_exclude = None if exclude is None else compile_pattern(exclude)
1727     gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1728     gs_extend_exclude = (
1729         None if extend_exclude is None else compile_pattern(extend_exclude)
1730     )
1731     gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1732     collected = black.get_sources(
1733         ctx=FakeContext(),
1734         src=gs_src,
1735         quiet=False,
1736         verbose=False,
1737         include=gs_include,
1738         exclude=gs_exclude,
1739         extend_exclude=gs_extend_exclude,
1740         force_exclude=gs_force_exclude,
1741         report=black.Report(),
1742         stdin_filename=stdin_filename,
1743     )
1744     assert sorted(list(collected)) == sorted(gs_expected)
1745
1746
1747 class TestFileCollection:
1748     def test_include_exclude(self) -> None:
1749         path = THIS_DIR / "data" / "include_exclude_tests"
1750         src = [path]
1751         expected = [
1752             Path(path / "b/dont_exclude/a.py"),
1753             Path(path / "b/dont_exclude/a.pyi"),
1754         ]
1755         assert_collected_sources(
1756             src,
1757             expected,
1758             include=r"\.pyi?$",
1759             exclude=r"/exclude/|/\.definitely_exclude/",
1760         )
1761
1762     def test_gitignore_used_as_default(self) -> None:
1763         base = Path(DATA_DIR / "include_exclude_tests")
1764         expected = [
1765             base / "b/.definitely_exclude/a.py",
1766             base / "b/.definitely_exclude/a.pyi",
1767         ]
1768         src = [base / "b/"]
1769         assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
1770
1771     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1772     def test_exclude_for_issue_1572(self) -> None:
1773         # Exclude shouldn't touch files that were explicitly given to Black through the
1774         # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1775         # https://github.com/psf/black/issues/1572
1776         path = DATA_DIR / "include_exclude_tests"
1777         src = [path / "b/exclude/a.py"]
1778         expected = [path / "b/exclude/a.py"]
1779         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1780
1781     def test_gitignore_exclude(self) -> None:
1782         path = THIS_DIR / "data" / "include_exclude_tests"
1783         include = re.compile(r"\.pyi?$")
1784         exclude = re.compile(r"")
1785         report = black.Report()
1786         gitignore = PathSpec.from_lines(
1787             "gitwildmatch", ["exclude/", ".definitely_exclude"]
1788         )
1789         sources: List[Path] = []
1790         expected = [
1791             Path(path / "b/dont_exclude/a.py"),
1792             Path(path / "b/dont_exclude/a.pyi"),
1793         ]
1794         this_abs = THIS_DIR.resolve()
1795         sources.extend(
1796             black.gen_python_files(
1797                 path.iterdir(),
1798                 this_abs,
1799                 include,
1800                 exclude,
1801                 None,
1802                 None,
1803                 report,
1804                 gitignore,
1805                 verbose=False,
1806                 quiet=False,
1807             )
1808         )
1809         assert sorted(expected) == sorted(sources)
1810
1811     def test_nested_gitignore(self) -> None:
1812         path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1813         include = re.compile(r"\.pyi?$")
1814         exclude = re.compile(r"")
1815         root_gitignore = black.files.get_gitignore(path)
1816         report = black.Report()
1817         expected: List[Path] = [
1818             Path(path / "x.py"),
1819             Path(path / "root/b.py"),
1820             Path(path / "root/c.py"),
1821             Path(path / "root/child/c.py"),
1822         ]
1823         this_abs = THIS_DIR.resolve()
1824         sources = list(
1825             black.gen_python_files(
1826                 path.iterdir(),
1827                 this_abs,
1828                 include,
1829                 exclude,
1830                 None,
1831                 None,
1832                 report,
1833                 root_gitignore,
1834                 verbose=False,
1835                 quiet=False,
1836             )
1837         )
1838         assert sorted(expected) == sorted(sources)
1839
1840     def test_invalid_gitignore(self) -> None:
1841         path = THIS_DIR / "data" / "invalid_gitignore_tests"
1842         empty_config = path / "pyproject.toml"
1843         result = BlackRunner().invoke(
1844             black.main, ["--verbose", "--config", str(empty_config), str(path)]
1845         )
1846         assert result.exit_code == 1
1847         assert result.stderr_bytes is not None
1848
1849         gitignore = path / ".gitignore"
1850         assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1851
1852     def test_invalid_nested_gitignore(self) -> None:
1853         path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1854         empty_config = path / "pyproject.toml"
1855         result = BlackRunner().invoke(
1856             black.main, ["--verbose", "--config", str(empty_config), str(path)]
1857         )
1858         assert result.exit_code == 1
1859         assert result.stderr_bytes is not None
1860
1861         gitignore = path / "a" / ".gitignore"
1862         assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1863
1864     def test_empty_include(self) -> None:
1865         path = DATA_DIR / "include_exclude_tests"
1866         src = [path]
1867         expected = [
1868             Path(path / "b/exclude/a.pie"),
1869             Path(path / "b/exclude/a.py"),
1870             Path(path / "b/exclude/a.pyi"),
1871             Path(path / "b/dont_exclude/a.pie"),
1872             Path(path / "b/dont_exclude/a.py"),
1873             Path(path / "b/dont_exclude/a.pyi"),
1874             Path(path / "b/.definitely_exclude/a.pie"),
1875             Path(path / "b/.definitely_exclude/a.py"),
1876             Path(path / "b/.definitely_exclude/a.pyi"),
1877             Path(path / ".gitignore"),
1878             Path(path / "pyproject.toml"),
1879         ]
1880         # Setting exclude explicitly to an empty string to block .gitignore usage.
1881         assert_collected_sources(src, expected, include="", exclude="")
1882
1883     def test_extend_exclude(self) -> None:
1884         path = DATA_DIR / "include_exclude_tests"
1885         src = [path]
1886         expected = [
1887             Path(path / "b/exclude/a.py"),
1888             Path(path / "b/dont_exclude/a.py"),
1889         ]
1890         assert_collected_sources(
1891             src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1892         )
1893
1894     def test_symlink_out_of_root_directory(self) -> None:
1895         path = MagicMock()
1896         root = THIS_DIR.resolve()
1897         child = MagicMock()
1898         include = re.compile(black.DEFAULT_INCLUDES)
1899         exclude = re.compile(black.DEFAULT_EXCLUDES)
1900         report = black.Report()
1901         gitignore = PathSpec.from_lines("gitwildmatch", [])
1902         # `child` should behave like a symlink which resolved path is clearly
1903         # outside of the `root` directory.
1904         path.iterdir.return_value = [child]
1905         child.resolve.return_value = Path("/a/b/c")
1906         child.as_posix.return_value = "/a/b/c"
1907         child.is_symlink.return_value = True
1908         try:
1909             list(
1910                 black.gen_python_files(
1911                     path.iterdir(),
1912                     root,
1913                     include,
1914                     exclude,
1915                     None,
1916                     None,
1917                     report,
1918                     gitignore,
1919                     verbose=False,
1920                     quiet=False,
1921                 )
1922             )
1923         except ValueError as ve:
1924             pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1925         path.iterdir.assert_called_once()
1926         child.resolve.assert_called_once()
1927         child.is_symlink.assert_called_once()
1928         # `child` should behave like a strange file which resolved path is clearly
1929         # outside of the `root` directory.
1930         child.is_symlink.return_value = False
1931         with pytest.raises(ValueError):
1932             list(
1933                 black.gen_python_files(
1934                     path.iterdir(),
1935                     root,
1936                     include,
1937                     exclude,
1938                     None,
1939                     None,
1940                     report,
1941                     gitignore,
1942                     verbose=False,
1943                     quiet=False,
1944                 )
1945             )
1946         path.iterdir.assert_called()
1947         assert path.iterdir.call_count == 2
1948         child.resolve.assert_called()
1949         assert child.resolve.call_count == 2
1950         child.is_symlink.assert_called()
1951         assert child.is_symlink.call_count == 2
1952
1953     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1954     def test_get_sources_with_stdin(self) -> None:
1955         src = ["-"]
1956         expected = ["-"]
1957         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1958
1959     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1960     def test_get_sources_with_stdin_filename(self) -> None:
1961         src = ["-"]
1962         stdin_filename = str(THIS_DIR / "data/collections.py")
1963         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1964         assert_collected_sources(
1965             src,
1966             expected,
1967             exclude=r"/exclude/a\.py",
1968             stdin_filename=stdin_filename,
1969         )
1970
1971     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1972     def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1973         # Exclude shouldn't exclude stdin_filename since it is mimicking the
1974         # file being passed directly. This is the same as
1975         # test_exclude_for_issue_1572
1976         path = DATA_DIR / "include_exclude_tests"
1977         src = ["-"]
1978         stdin_filename = str(path / "b/exclude/a.py")
1979         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1980         assert_collected_sources(
1981             src,
1982             expected,
1983             exclude=r"/exclude/|a\.py",
1984             stdin_filename=stdin_filename,
1985         )
1986
1987     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1988     def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1989         # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1990         # file being passed directly. This is the same as
1991         # test_exclude_for_issue_1572
1992         src = ["-"]
1993         path = THIS_DIR / "data" / "include_exclude_tests"
1994         stdin_filename = str(path / "b/exclude/a.py")
1995         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1996         assert_collected_sources(
1997             src,
1998             expected,
1999             extend_exclude=r"/exclude/|a\.py",
2000             stdin_filename=stdin_filename,
2001         )
2002
2003     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2004     def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2005         # Force exclude should exclude the file when passing it through
2006         # stdin_filename
2007         path = THIS_DIR / "data" / "include_exclude_tests"
2008         stdin_filename = str(path / "b/exclude/a.py")
2009         assert_collected_sources(
2010             src=["-"],
2011             expected=[],
2012             force_exclude=r"/exclude/|a\.py",
2013             stdin_filename=stdin_filename,
2014         )
2015
2016
2017 with open(black.__file__, "r", encoding="utf-8") as _bf:
2018     black_source_lines = _bf.readlines()
2019
2020
2021 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2022     """Show function calls `from black/__init__.py` as they happen.
2023
2024     Register this with `sys.settrace()` in a test you're debugging.
2025     """
2026     if event != "call":
2027         return tracefunc
2028
2029     stack = len(inspect.stack()) - 19
2030     stack *= 2
2031     filename = frame.f_code.co_filename
2032     lineno = frame.f_lineno
2033     func_sig_lineno = lineno - 1
2034     funcname = black_source_lines[func_sig_lineno].strip()
2035     while funcname.startswith("@"):
2036         func_sig_lineno += 1
2037         funcname = black_source_lines[func_sig_lineno].strip()
2038     if "black/__init__.py" in filename:
2039         print(f"{' ' * stack}{lineno}:{funcname}")
2040     return tracefunc