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

MNT: remove unnecessary test deps + some refactoring (GH-2510)
[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
809     def test_get_future_imports(self) -> None:
810         node = black.lib2to3_parse("\n")
811         self.assertEqual(set(), black.get_future_imports(node))
812         node = black.lib2to3_parse("from __future__ import black\n")
813         self.assertEqual({"black"}, black.get_future_imports(node))
814         node = black.lib2to3_parse("from __future__ import multiple, imports\n")
815         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
816         node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
817         self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
818         node = black.lib2to3_parse(
819             "from __future__ import multiple\nfrom __future__ import imports\n"
820         )
821         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
822         node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
823         self.assertEqual({"black"}, black.get_future_imports(node))
824         node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
825         self.assertEqual({"black"}, black.get_future_imports(node))
826         node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
827         self.assertEqual(set(), black.get_future_imports(node))
828         node = black.lib2to3_parse("from some.module import black\n")
829         self.assertEqual(set(), black.get_future_imports(node))
830         node = black.lib2to3_parse(
831             "from __future__ import unicode_literals as _unicode_literals"
832         )
833         self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
834         node = black.lib2to3_parse(
835             "from __future__ import unicode_literals as _lol, print"
836         )
837         self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
838
839     def test_debug_visitor(self) -> None:
840         source, _ = read_data("debug_visitor.py")
841         expected, _ = read_data("debug_visitor.out")
842         out_lines = []
843         err_lines = []
844
845         def out(msg: str, **kwargs: Any) -> None:
846             out_lines.append(msg)
847
848         def err(msg: str, **kwargs: Any) -> None:
849             err_lines.append(msg)
850
851         with patch("black.debug.out", out):
852             DebugVisitor.show(source)
853         actual = "\n".join(out_lines) + "\n"
854         log_name = ""
855         if expected != actual:
856             log_name = black.dump_to_file(*out_lines)
857         self.assertEqual(
858             expected,
859             actual,
860             f"AST print out is different. Actual version dumped to {log_name}",
861         )
862
863     def test_format_file_contents(self) -> None:
864         empty = ""
865         mode = DEFAULT_MODE
866         with self.assertRaises(black.NothingChanged):
867             black.format_file_contents(empty, mode=mode, fast=False)
868         just_nl = "\n"
869         with self.assertRaises(black.NothingChanged):
870             black.format_file_contents(just_nl, mode=mode, fast=False)
871         same = "j = [1, 2, 3]\n"
872         with self.assertRaises(black.NothingChanged):
873             black.format_file_contents(same, mode=mode, fast=False)
874         different = "j = [1,2,3]"
875         expected = same
876         actual = black.format_file_contents(different, mode=mode, fast=False)
877         self.assertEqual(expected, actual)
878         invalid = "return if you can"
879         with self.assertRaises(black.InvalidInput) as e:
880             black.format_file_contents(invalid, mode=mode, fast=False)
881         self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
882
883     def test_endmarker(self) -> None:
884         n = black.lib2to3_parse("\n")
885         self.assertEqual(n.type, black.syms.file_input)
886         self.assertEqual(len(n.children), 1)
887         self.assertEqual(n.children[0].type, black.token.ENDMARKER)
888
889     @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
890     def test_assertFormatEqual(self) -> None:
891         out_lines = []
892         err_lines = []
893
894         def out(msg: str, **kwargs: Any) -> None:
895             out_lines.append(msg)
896
897         def err(msg: str, **kwargs: Any) -> None:
898             err_lines.append(msg)
899
900         with patch("black.output._out", out), patch("black.output._err", err):
901             with self.assertRaises(AssertionError):
902                 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
903
904         out_str = "".join(out_lines)
905         self.assertTrue("Expected tree:" in out_str)
906         self.assertTrue("Actual tree:" in out_str)
907         self.assertEqual("".join(err_lines), "")
908
909     @event_loop()
910     @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
911     def test_works_in_mono_process_only_environment(self) -> None:
912         with cache_dir() as workspace:
913             for f in [
914                 (workspace / "one.py").resolve(),
915                 (workspace / "two.py").resolve(),
916             ]:
917                 f.write_text('print("hello")\n')
918             self.invokeBlack([str(workspace)])
919
920     @event_loop()
921     def test_check_diff_use_together(self) -> None:
922         with cache_dir():
923             # Files which will be reformatted.
924             src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
925             self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
926             # Files which will not be reformatted.
927             src2 = (THIS_DIR / "data" / "composition.py").resolve()
928             self.invokeBlack([str(src2), "--diff", "--check"])
929             # Multi file command.
930             self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
931
932     def test_no_files(self) -> None:
933         with cache_dir():
934             # Without an argument, black exits with error code 0.
935             self.invokeBlack([])
936
937     def test_broken_symlink(self) -> None:
938         with cache_dir() as workspace:
939             symlink = workspace / "broken_link.py"
940             try:
941                 symlink.symlink_to("nonexistent.py")
942             except OSError as e:
943                 self.skipTest(f"Can't create symlinks: {e}")
944             self.invokeBlack([str(workspace.resolve())])
945
946     def test_single_file_force_pyi(self) -> None:
947         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
948         contents, expected = read_data("force_pyi")
949         with cache_dir() as workspace:
950             path = (workspace / "file.py").resolve()
951             with open(path, "w") as fh:
952                 fh.write(contents)
953             self.invokeBlack([str(path), "--pyi"])
954             with open(path, "r") as fh:
955                 actual = fh.read()
956             # verify cache with --pyi is separate
957             pyi_cache = black.read_cache(pyi_mode)
958             self.assertIn(str(path), pyi_cache)
959             normal_cache = black.read_cache(DEFAULT_MODE)
960             self.assertNotIn(str(path), normal_cache)
961         self.assertFormatEqual(expected, actual)
962         black.assert_equivalent(contents, actual)
963         black.assert_stable(contents, actual, pyi_mode)
964
965     @event_loop()
966     def test_multi_file_force_pyi(self) -> None:
967         reg_mode = DEFAULT_MODE
968         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
969         contents, expected = read_data("force_pyi")
970         with cache_dir() as workspace:
971             paths = [
972                 (workspace / "file1.py").resolve(),
973                 (workspace / "file2.py").resolve(),
974             ]
975             for path in paths:
976                 with open(path, "w") as fh:
977                     fh.write(contents)
978             self.invokeBlack([str(p) for p in paths] + ["--pyi"])
979             for path in paths:
980                 with open(path, "r") as fh:
981                     actual = fh.read()
982                 self.assertEqual(actual, expected)
983             # verify cache with --pyi is separate
984             pyi_cache = black.read_cache(pyi_mode)
985             normal_cache = black.read_cache(reg_mode)
986             for path in paths:
987                 self.assertIn(str(path), pyi_cache)
988                 self.assertNotIn(str(path), normal_cache)
989
990     def test_pipe_force_pyi(self) -> None:
991         source, expected = read_data("force_pyi")
992         result = CliRunner().invoke(
993             black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
994         )
995         self.assertEqual(result.exit_code, 0)
996         actual = result.output
997         self.assertFormatEqual(actual, expected)
998
999     def test_single_file_force_py36(self) -> None:
1000         reg_mode = DEFAULT_MODE
1001         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1002         source, expected = read_data("force_py36")
1003         with cache_dir() as workspace:
1004             path = (workspace / "file.py").resolve()
1005             with open(path, "w") as fh:
1006                 fh.write(source)
1007             self.invokeBlack([str(path), *PY36_ARGS])
1008             with open(path, "r") as fh:
1009                 actual = fh.read()
1010             # verify cache with --target-version is separate
1011             py36_cache = black.read_cache(py36_mode)
1012             self.assertIn(str(path), py36_cache)
1013             normal_cache = black.read_cache(reg_mode)
1014             self.assertNotIn(str(path), normal_cache)
1015         self.assertEqual(actual, expected)
1016
1017     @event_loop()
1018     def test_multi_file_force_py36(self) -> None:
1019         reg_mode = DEFAULT_MODE
1020         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1021         source, expected = read_data("force_py36")
1022         with cache_dir() as workspace:
1023             paths = [
1024                 (workspace / "file1.py").resolve(),
1025                 (workspace / "file2.py").resolve(),
1026             ]
1027             for path in paths:
1028                 with open(path, "w") as fh:
1029                     fh.write(source)
1030             self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1031             for path in paths:
1032                 with open(path, "r") as fh:
1033                     actual = fh.read()
1034                 self.assertEqual(actual, expected)
1035             # verify cache with --target-version is separate
1036             pyi_cache = black.read_cache(py36_mode)
1037             normal_cache = black.read_cache(reg_mode)
1038             for path in paths:
1039                 self.assertIn(str(path), pyi_cache)
1040                 self.assertNotIn(str(path), normal_cache)
1041
1042     def test_pipe_force_py36(self) -> None:
1043         source, expected = read_data("force_py36")
1044         result = CliRunner().invoke(
1045             black.main,
1046             ["-", "-q", "--target-version=py36"],
1047             input=BytesIO(source.encode("utf8")),
1048         )
1049         self.assertEqual(result.exit_code, 0)
1050         actual = result.output
1051         self.assertFormatEqual(actual, expected)
1052
1053     def test_reformat_one_with_stdin(self) -> None:
1054         with patch(
1055             "black.format_stdin_to_stdout",
1056             return_value=lambda *args, **kwargs: black.Changed.YES,
1057         ) as fsts:
1058             report = MagicMock()
1059             path = Path("-")
1060             black.reformat_one(
1061                 path,
1062                 fast=True,
1063                 write_back=black.WriteBack.YES,
1064                 mode=DEFAULT_MODE,
1065                 report=report,
1066             )
1067             fsts.assert_called_once()
1068             report.done.assert_called_with(path, black.Changed.YES)
1069
1070     def test_reformat_one_with_stdin_filename(self) -> None:
1071         with patch(
1072             "black.format_stdin_to_stdout",
1073             return_value=lambda *args, **kwargs: black.Changed.YES,
1074         ) as fsts:
1075             report = MagicMock()
1076             p = "foo.py"
1077             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1078             expected = Path(p)
1079             black.reformat_one(
1080                 path,
1081                 fast=True,
1082                 write_back=black.WriteBack.YES,
1083                 mode=DEFAULT_MODE,
1084                 report=report,
1085             )
1086             fsts.assert_called_once_with(
1087                 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1088             )
1089             # __BLACK_STDIN_FILENAME__ should have been stripped
1090             report.done.assert_called_with(expected, black.Changed.YES)
1091
1092     def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1093         with patch(
1094             "black.format_stdin_to_stdout",
1095             return_value=lambda *args, **kwargs: black.Changed.YES,
1096         ) as fsts:
1097             report = MagicMock()
1098             p = "foo.pyi"
1099             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1100             expected = Path(p)
1101             black.reformat_one(
1102                 path,
1103                 fast=True,
1104                 write_back=black.WriteBack.YES,
1105                 mode=DEFAULT_MODE,
1106                 report=report,
1107             )
1108             fsts.assert_called_once_with(
1109                 fast=True,
1110                 write_back=black.WriteBack.YES,
1111                 mode=replace(DEFAULT_MODE, is_pyi=True),
1112             )
1113             # __BLACK_STDIN_FILENAME__ should have been stripped
1114             report.done.assert_called_with(expected, black.Changed.YES)
1115
1116     def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1117         with patch(
1118             "black.format_stdin_to_stdout",
1119             return_value=lambda *args, **kwargs: black.Changed.YES,
1120         ) as fsts:
1121             report = MagicMock()
1122             p = "foo.ipynb"
1123             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1124             expected = Path(p)
1125             black.reformat_one(
1126                 path,
1127                 fast=True,
1128                 write_back=black.WriteBack.YES,
1129                 mode=DEFAULT_MODE,
1130                 report=report,
1131             )
1132             fsts.assert_called_once_with(
1133                 fast=True,
1134                 write_back=black.WriteBack.YES,
1135                 mode=replace(DEFAULT_MODE, is_ipynb=True),
1136             )
1137             # __BLACK_STDIN_FILENAME__ should have been stripped
1138             report.done.assert_called_with(expected, black.Changed.YES)
1139
1140     def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1141         with patch(
1142             "black.format_stdin_to_stdout",
1143             return_value=lambda *args, **kwargs: black.Changed.YES,
1144         ) as fsts:
1145             report = MagicMock()
1146             # Even with an existing file, since we are forcing stdin, black
1147             # should output to stdout and not modify the file inplace
1148             p = Path(str(THIS_DIR / "data/collections.py"))
1149             # Make sure is_file actually returns True
1150             self.assertTrue(p.is_file())
1151             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1152             expected = Path(p)
1153             black.reformat_one(
1154                 path,
1155                 fast=True,
1156                 write_back=black.WriteBack.YES,
1157                 mode=DEFAULT_MODE,
1158                 report=report,
1159             )
1160             fsts.assert_called_once()
1161             # __BLACK_STDIN_FILENAME__ should have been stripped
1162             report.done.assert_called_with(expected, black.Changed.YES)
1163
1164     def test_reformat_one_with_stdin_empty(self) -> None:
1165         output = io.StringIO()
1166         with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1167             try:
1168                 black.format_stdin_to_stdout(
1169                     fast=True,
1170                     content="",
1171                     write_back=black.WriteBack.YES,
1172                     mode=DEFAULT_MODE,
1173                 )
1174             except io.UnsupportedOperation:
1175                 pass  # StringIO does not support detach
1176             assert output.getvalue() == ""
1177
1178     def test_invalid_cli_regex(self) -> None:
1179         for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1180             self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1181
1182     def test_required_version_matches_version(self) -> None:
1183         self.invokeBlack(
1184             ["--required-version", black.__version__], exit_code=0, ignore_config=True
1185         )
1186
1187     def test_required_version_does_not_match_version(self) -> None:
1188         self.invokeBlack(
1189             ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1190         )
1191
1192     def test_preserves_line_endings(self) -> None:
1193         with TemporaryDirectory() as workspace:
1194             test_file = Path(workspace) / "test.py"
1195             for nl in ["\n", "\r\n"]:
1196                 contents = nl.join(["def f(  ):", "    pass"])
1197                 test_file.write_bytes(contents.encode())
1198                 ff(test_file, write_back=black.WriteBack.YES)
1199                 updated_contents: bytes = test_file.read_bytes()
1200                 self.assertIn(nl.encode(), updated_contents)
1201                 if nl == "\n":
1202                     self.assertNotIn(b"\r\n", updated_contents)
1203
1204     def test_preserves_line_endings_via_stdin(self) -> None:
1205         for nl in ["\n", "\r\n"]:
1206             contents = nl.join(["def f(  ):", "    pass"])
1207             runner = BlackRunner()
1208             result = runner.invoke(
1209                 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1210             )
1211             self.assertEqual(result.exit_code, 0)
1212             output = result.stdout_bytes
1213             self.assertIn(nl.encode("utf8"), output)
1214             if nl == "\n":
1215                 self.assertNotIn(b"\r\n", output)
1216
1217     def test_assert_equivalent_different_asts(self) -> None:
1218         with self.assertRaises(AssertionError):
1219             black.assert_equivalent("{}", "None")
1220
1221     def test_shhh_click(self) -> None:
1222         try:
1223             from click import _unicodefun
1224         except ModuleNotFoundError:
1225             self.skipTest("Incompatible Click version")
1226         if not hasattr(_unicodefun, "_verify_python3_env"):
1227             self.skipTest("Incompatible Click version")
1228         # First, let's see if Click is crashing with a preferred ASCII charset.
1229         with patch("locale.getpreferredencoding") as gpe:
1230             gpe.return_value = "ASCII"
1231             with self.assertRaises(RuntimeError):
1232                 _unicodefun._verify_python3_env()  # type: ignore
1233         # Now, let's silence Click...
1234         black.patch_click()
1235         # ...and confirm it's silent.
1236         with patch("locale.getpreferredencoding") as gpe:
1237             gpe.return_value = "ASCII"
1238             try:
1239                 _unicodefun._verify_python3_env()  # type: ignore
1240             except RuntimeError as re:
1241                 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1242
1243     def test_root_logger_not_used_directly(self) -> None:
1244         def fail(*args: Any, **kwargs: Any) -> None:
1245             self.fail("Record created with root logger")
1246
1247         with patch.multiple(
1248             logging.root,
1249             debug=fail,
1250             info=fail,
1251             warning=fail,
1252             error=fail,
1253             critical=fail,
1254             log=fail,
1255         ):
1256             ff(THIS_DIR / "util.py")
1257
1258     def test_invalid_config_return_code(self) -> None:
1259         tmp_file = Path(black.dump_to_file())
1260         try:
1261             tmp_config = Path(black.dump_to_file())
1262             tmp_config.unlink()
1263             args = ["--config", str(tmp_config), str(tmp_file)]
1264             self.invokeBlack(args, exit_code=2, ignore_config=False)
1265         finally:
1266             tmp_file.unlink()
1267
1268     def test_parse_pyproject_toml(self) -> None:
1269         test_toml_file = THIS_DIR / "test.toml"
1270         config = black.parse_pyproject_toml(str(test_toml_file))
1271         self.assertEqual(config["verbose"], 1)
1272         self.assertEqual(config["check"], "no")
1273         self.assertEqual(config["diff"], "y")
1274         self.assertEqual(config["color"], True)
1275         self.assertEqual(config["line_length"], 79)
1276         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1277         self.assertEqual(config["exclude"], r"\.pyi?$")
1278         self.assertEqual(config["include"], r"\.py?$")
1279
1280     def test_read_pyproject_toml(self) -> None:
1281         test_toml_file = THIS_DIR / "test.toml"
1282         fake_ctx = FakeContext()
1283         black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1284         config = fake_ctx.default_map
1285         self.assertEqual(config["verbose"], "1")
1286         self.assertEqual(config["check"], "no")
1287         self.assertEqual(config["diff"], "y")
1288         self.assertEqual(config["color"], "True")
1289         self.assertEqual(config["line_length"], "79")
1290         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1291         self.assertEqual(config["exclude"], r"\.pyi?$")
1292         self.assertEqual(config["include"], r"\.py?$")
1293
1294     def test_find_project_root(self) -> None:
1295         with TemporaryDirectory() as workspace:
1296             root = Path(workspace)
1297             test_dir = root / "test"
1298             test_dir.mkdir()
1299
1300             src_dir = root / "src"
1301             src_dir.mkdir()
1302
1303             root_pyproject = root / "pyproject.toml"
1304             root_pyproject.touch()
1305             src_pyproject = src_dir / "pyproject.toml"
1306             src_pyproject.touch()
1307             src_python = src_dir / "foo.py"
1308             src_python.touch()
1309
1310             self.assertEqual(
1311                 black.find_project_root((src_dir, test_dir)), root.resolve()
1312             )
1313             self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1314             self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1315
1316     @patch(
1317         "black.files.find_user_pyproject_toml",
1318         black.files.find_user_pyproject_toml.__wrapped__,
1319     )
1320     def test_find_user_pyproject_toml_linux(self) -> None:
1321         if system() == "Windows":
1322             return
1323
1324         # Test if XDG_CONFIG_HOME is checked
1325         with TemporaryDirectory() as workspace:
1326             tmp_user_config = Path(workspace) / "black"
1327             with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1328                 self.assertEqual(
1329                     black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1330                 )
1331
1332         # Test fallback for XDG_CONFIG_HOME
1333         with patch.dict("os.environ"):
1334             os.environ.pop("XDG_CONFIG_HOME", None)
1335             fallback_user_config = Path("~/.config").expanduser() / "black"
1336             self.assertEqual(
1337                 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1338             )
1339
1340     def test_find_user_pyproject_toml_windows(self) -> None:
1341         if system() != "Windows":
1342             return
1343
1344         user_config_path = Path.home() / ".black"
1345         self.assertEqual(
1346             black.files.find_user_pyproject_toml(), user_config_path.resolve()
1347         )
1348
1349     def test_bpo_33660_workaround(self) -> None:
1350         if system() == "Windows":
1351             return
1352
1353         # https://bugs.python.org/issue33660
1354         root = Path("/")
1355         with change_directory(root):
1356             path = Path("workspace") / "project"
1357             report = black.Report(verbose=True)
1358             normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1359             self.assertEqual(normalized_path, "workspace/project")
1360
1361     def test_newline_comment_interaction(self) -> None:
1362         source = "class A:\\\r\n# type: ignore\n pass\n"
1363         output = black.format_str(source, mode=DEFAULT_MODE)
1364         black.assert_stable(source, output, mode=DEFAULT_MODE)
1365
1366     def test_bpo_2142_workaround(self) -> None:
1367
1368         # https://bugs.python.org/issue2142
1369
1370         source, _ = read_data("missing_final_newline.py")
1371         # read_data adds a trailing newline
1372         source = source.rstrip()
1373         expected, _ = read_data("missing_final_newline.diff")
1374         tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1375         diff_header = re.compile(
1376             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1377             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1378         )
1379         try:
1380             result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1381             self.assertEqual(result.exit_code, 0)
1382         finally:
1383             os.unlink(tmp_file)
1384         actual = result.output
1385         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1386         self.assertEqual(actual, expected)
1387
1388     @pytest.mark.python2
1389     def test_docstring_reformat_for_py27(self) -> None:
1390         """
1391         Check that stripping trailing whitespace from Python 2 docstrings
1392         doesn't trigger a "not equivalent to source" error
1393         """
1394         source = (
1395             b'def foo():\r\n    """Testing\r\n    Testing """\r\n    print "Foo"\r\n'
1396         )
1397         expected = 'def foo():\n    """Testing\n    Testing"""\n    print "Foo"\n'
1398
1399         result = CliRunner().invoke(
1400             black.main,
1401             ["-", "-q", "--target-version=py27"],
1402             input=BytesIO(source),
1403         )
1404
1405         self.assertEqual(result.exit_code, 0)
1406         actual = result.output
1407         self.assertFormatEqual(actual, expected)
1408
1409     @staticmethod
1410     def compare_results(
1411         result: click.testing.Result, expected_value: str, expected_exit_code: int
1412     ) -> None:
1413         """Helper method to test the value and exit code of a click Result."""
1414         assert (
1415             result.output == expected_value
1416         ), "The output did not match the expected value."
1417         assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1418
1419     def test_code_option(self) -> None:
1420         """Test the code option with no changes."""
1421         code = 'print("Hello world")\n'
1422         args = ["--code", code]
1423         result = CliRunner().invoke(black.main, args)
1424
1425         self.compare_results(result, code, 0)
1426
1427     def test_code_option_changed(self) -> None:
1428         """Test the code option when changes are required."""
1429         code = "print('hello world')"
1430         formatted = black.format_str(code, mode=DEFAULT_MODE)
1431
1432         args = ["--code", code]
1433         result = CliRunner().invoke(black.main, args)
1434
1435         self.compare_results(result, formatted, 0)
1436
1437     def test_code_option_check(self) -> None:
1438         """Test the code option when check is passed."""
1439         args = ["--check", "--code", 'print("Hello world")\n']
1440         result = CliRunner().invoke(black.main, args)
1441         self.compare_results(result, "", 0)
1442
1443     def test_code_option_check_changed(self) -> None:
1444         """Test the code option when changes are required, and check is passed."""
1445         args = ["--check", "--code", "print('hello world')"]
1446         result = CliRunner().invoke(black.main, args)
1447         self.compare_results(result, "", 1)
1448
1449     def test_code_option_diff(self) -> None:
1450         """Test the code option when diff is passed."""
1451         code = "print('hello world')"
1452         formatted = black.format_str(code, mode=DEFAULT_MODE)
1453         result_diff = diff(code, formatted, "STDIN", "STDOUT")
1454
1455         args = ["--diff", "--code", code]
1456         result = CliRunner().invoke(black.main, args)
1457
1458         # Remove time from diff
1459         output = DIFF_TIME.sub("", result.output)
1460
1461         assert output == result_diff, "The output did not match the expected value."
1462         assert result.exit_code == 0, "The exit code is incorrect."
1463
1464     def test_code_option_color_diff(self) -> None:
1465         """Test the code option when color and diff are passed."""
1466         code = "print('hello world')"
1467         formatted = black.format_str(code, mode=DEFAULT_MODE)
1468
1469         result_diff = diff(code, formatted, "STDIN", "STDOUT")
1470         result_diff = color_diff(result_diff)
1471
1472         args = ["--diff", "--color", "--code", code]
1473         result = CliRunner().invoke(black.main, args)
1474
1475         # Remove time from diff
1476         output = DIFF_TIME.sub("", result.output)
1477
1478         assert output == result_diff, "The output did not match the expected value."
1479         assert result.exit_code == 0, "The exit code is incorrect."
1480
1481     def test_code_option_safe(self) -> None:
1482         """Test that the code option throws an error when the sanity checks fail."""
1483         # Patch black.assert_equivalent to ensure the sanity checks fail
1484         with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1485             code = 'print("Hello world")'
1486             error_msg = f"{code}\nerror: cannot format <string>: \n"
1487
1488             args = ["--safe", "--code", code]
1489             result = CliRunner().invoke(black.main, args)
1490
1491             self.compare_results(result, error_msg, 123)
1492
1493     def test_code_option_fast(self) -> None:
1494         """Test that the code option ignores errors when the sanity checks fail."""
1495         # Patch black.assert_equivalent to ensure the sanity checks fail
1496         with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1497             code = 'print("Hello world")'
1498             formatted = black.format_str(code, mode=DEFAULT_MODE)
1499
1500             args = ["--fast", "--code", code]
1501             result = CliRunner().invoke(black.main, args)
1502
1503             self.compare_results(result, formatted, 0)
1504
1505     def test_code_option_config(self) -> None:
1506         """
1507         Test that the code option finds the pyproject.toml in the current directory.
1508         """
1509         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1510             args = ["--code", "print"]
1511             CliRunner().invoke(black.main, args)
1512
1513             pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve()
1514             assert (
1515                 len(parse.mock_calls) >= 1
1516             ), "Expected config parse to be called with the current directory."
1517
1518             _, call_args, _ = parse.mock_calls[0]
1519             assert (
1520                 call_args[0].lower() == str(pyproject_path).lower()
1521             ), "Incorrect config loaded."
1522
1523     def test_code_option_parent_config(self) -> None:
1524         """
1525         Test that the code option finds the pyproject.toml in the parent directory.
1526         """
1527         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1528             with change_directory(Path("tests")):
1529                 args = ["--code", "print"]
1530                 CliRunner().invoke(black.main, args)
1531
1532                 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1533                 assert (
1534                     len(parse.mock_calls) >= 1
1535                 ), "Expected config parse to be called with the current directory."
1536
1537                 _, call_args, _ = parse.mock_calls[0]
1538                 assert (
1539                     call_args[0].lower() == str(pyproject_path).lower()
1540                 ), "Incorrect config loaded."
1541
1542
1543 class TestCaching:
1544     def test_cache_broken_file(self) -> None:
1545         mode = DEFAULT_MODE
1546         with cache_dir() as workspace:
1547             cache_file = get_cache_file(mode)
1548             cache_file.write_text("this is not a pickle")
1549             assert black.read_cache(mode) == {}
1550             src = (workspace / "test.py").resolve()
1551             src.write_text("print('hello')")
1552             invokeBlack([str(src)])
1553             cache = black.read_cache(mode)
1554             assert str(src) in cache
1555
1556     def test_cache_single_file_already_cached(self) -> None:
1557         mode = DEFAULT_MODE
1558         with cache_dir() as workspace:
1559             src = (workspace / "test.py").resolve()
1560             src.write_text("print('hello')")
1561             black.write_cache({}, [src], mode)
1562             invokeBlack([str(src)])
1563             assert src.read_text() == "print('hello')"
1564
1565     @event_loop()
1566     def test_cache_multiple_files(self) -> None:
1567         mode = DEFAULT_MODE
1568         with cache_dir() as workspace, patch(
1569             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1570         ):
1571             one = (workspace / "one.py").resolve()
1572             with one.open("w") as fobj:
1573                 fobj.write("print('hello')")
1574             two = (workspace / "two.py").resolve()
1575             with two.open("w") as fobj:
1576                 fobj.write("print('hello')")
1577             black.write_cache({}, [one], mode)
1578             invokeBlack([str(workspace)])
1579             with one.open("r") as fobj:
1580                 assert fobj.read() == "print('hello')"
1581             with two.open("r") as fobj:
1582                 assert fobj.read() == 'print("hello")\n'
1583             cache = black.read_cache(mode)
1584             assert str(one) in cache
1585             assert str(two) in cache
1586
1587     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1588     def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1589         mode = DEFAULT_MODE
1590         with cache_dir() as workspace:
1591             src = (workspace / "test.py").resolve()
1592             with src.open("w") as fobj:
1593                 fobj.write("print('hello')")
1594             with patch("black.read_cache") as read_cache, patch(
1595                 "black.write_cache"
1596             ) as write_cache:
1597                 cmd = [str(src), "--diff"]
1598                 if color:
1599                     cmd.append("--color")
1600                 invokeBlack(cmd)
1601                 cache_file = get_cache_file(mode)
1602                 assert cache_file.exists() is False
1603                 write_cache.assert_not_called()
1604                 read_cache.assert_not_called()
1605
1606     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1607     @event_loop()
1608     def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1609         with cache_dir() as workspace:
1610             for tag in range(0, 4):
1611                 src = (workspace / f"test{tag}.py").resolve()
1612                 with src.open("w") as fobj:
1613                     fobj.write("print('hello')")
1614             with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1615                 cmd = ["--diff", str(workspace)]
1616                 if color:
1617                     cmd.append("--color")
1618                 invokeBlack(cmd, exit_code=0)
1619                 # this isn't quite doing what we want, but if it _isn't_
1620                 # called then we cannot be using the lock it provides
1621                 mgr.assert_called()
1622
1623     def test_no_cache_when_stdin(self) -> None:
1624         mode = DEFAULT_MODE
1625         with cache_dir():
1626             result = CliRunner().invoke(
1627                 black.main, ["-"], input=BytesIO(b"print('hello')")
1628             )
1629             assert not result.exit_code
1630             cache_file = get_cache_file(mode)
1631             assert not cache_file.exists()
1632
1633     def test_read_cache_no_cachefile(self) -> None:
1634         mode = DEFAULT_MODE
1635         with cache_dir():
1636             assert black.read_cache(mode) == {}
1637
1638     def test_write_cache_read_cache(self) -> None:
1639         mode = DEFAULT_MODE
1640         with cache_dir() as workspace:
1641             src = (workspace / "test.py").resolve()
1642             src.touch()
1643             black.write_cache({}, [src], mode)
1644             cache = black.read_cache(mode)
1645             assert str(src) in cache
1646             assert cache[str(src)] == black.get_cache_info(src)
1647
1648     def test_filter_cached(self) -> None:
1649         with TemporaryDirectory() as workspace:
1650             path = Path(workspace)
1651             uncached = (path / "uncached").resolve()
1652             cached = (path / "cached").resolve()
1653             cached_but_changed = (path / "changed").resolve()
1654             uncached.touch()
1655             cached.touch()
1656             cached_but_changed.touch()
1657             cache = {
1658                 str(cached): black.get_cache_info(cached),
1659                 str(cached_but_changed): (0.0, 0),
1660             }
1661             todo, done = black.filter_cached(
1662                 cache, {uncached, cached, cached_but_changed}
1663             )
1664             assert todo == {uncached, cached_but_changed}
1665             assert done == {cached}
1666
1667     def test_write_cache_creates_directory_if_needed(self) -> None:
1668         mode = DEFAULT_MODE
1669         with cache_dir(exists=False) as workspace:
1670             assert not workspace.exists()
1671             black.write_cache({}, [], mode)
1672             assert workspace.exists()
1673
1674     @event_loop()
1675     def test_failed_formatting_does_not_get_cached(self) -> None:
1676         mode = DEFAULT_MODE
1677         with cache_dir() as workspace, patch(
1678             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1679         ):
1680             failing = (workspace / "failing.py").resolve()
1681             with failing.open("w") as fobj:
1682                 fobj.write("not actually python")
1683             clean = (workspace / "clean.py").resolve()
1684             with clean.open("w") as fobj:
1685                 fobj.write('print("hello")\n')
1686             invokeBlack([str(workspace)], exit_code=123)
1687             cache = black.read_cache(mode)
1688             assert str(failing) not in cache
1689             assert str(clean) in cache
1690
1691     def test_write_cache_write_fail(self) -> None:
1692         mode = DEFAULT_MODE
1693         with cache_dir(), patch.object(Path, "open") as mock:
1694             mock.side_effect = OSError
1695             black.write_cache({}, [], mode)
1696
1697     def test_read_cache_line_lengths(self) -> None:
1698         mode = DEFAULT_MODE
1699         short_mode = replace(DEFAULT_MODE, line_length=1)
1700         with cache_dir() as workspace:
1701             path = (workspace / "file.py").resolve()
1702             path.touch()
1703             black.write_cache({}, [path], mode)
1704             one = black.read_cache(mode)
1705             assert str(path) in one
1706             two = black.read_cache(short_mode)
1707             assert str(path) not in two
1708
1709
1710 def assert_collected_sources(
1711     src: Sequence[Union[str, Path]],
1712     expected: Sequence[Union[str, Path]],
1713     *,
1714     exclude: Optional[str] = None,
1715     include: Optional[str] = None,
1716     extend_exclude: Optional[str] = None,
1717     force_exclude: Optional[str] = None,
1718     stdin_filename: Optional[str] = None,
1719 ) -> None:
1720     gs_src = tuple(str(Path(s)) for s in src)
1721     gs_expected = [Path(s) for s in expected]
1722     gs_exclude = None if exclude is None else compile_pattern(exclude)
1723     gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1724     gs_extend_exclude = (
1725         None if extend_exclude is None else compile_pattern(extend_exclude)
1726     )
1727     gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1728     collected = black.get_sources(
1729         ctx=FakeContext(),
1730         src=gs_src,
1731         quiet=False,
1732         verbose=False,
1733         include=gs_include,
1734         exclude=gs_exclude,
1735         extend_exclude=gs_extend_exclude,
1736         force_exclude=gs_force_exclude,
1737         report=black.Report(),
1738         stdin_filename=stdin_filename,
1739     )
1740     assert sorted(list(collected)) == sorted(gs_expected)
1741
1742
1743 class TestFileCollection:
1744     def test_include_exclude(self) -> None:
1745         path = THIS_DIR / "data" / "include_exclude_tests"
1746         src = [path]
1747         expected = [
1748             Path(path / "b/dont_exclude/a.py"),
1749             Path(path / "b/dont_exclude/a.pyi"),
1750         ]
1751         assert_collected_sources(
1752             src,
1753             expected,
1754             include=r"\.pyi?$",
1755             exclude=r"/exclude/|/\.definitely_exclude/",
1756         )
1757
1758     def test_gitignore_used_as_default(self) -> None:
1759         base = Path(DATA_DIR / "include_exclude_tests")
1760         expected = [
1761             base / "b/.definitely_exclude/a.py",
1762             base / "b/.definitely_exclude/a.pyi",
1763         ]
1764         src = [base / "b/"]
1765         assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
1766
1767     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1768     def test_exclude_for_issue_1572(self) -> None:
1769         # Exclude shouldn't touch files that were explicitly given to Black through the
1770         # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1771         # https://github.com/psf/black/issues/1572
1772         path = DATA_DIR / "include_exclude_tests"
1773         src = [path / "b/exclude/a.py"]
1774         expected = [path / "b/exclude/a.py"]
1775         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1776
1777     def test_gitignore_exclude(self) -> None:
1778         path = THIS_DIR / "data" / "include_exclude_tests"
1779         include = re.compile(r"\.pyi?$")
1780         exclude = re.compile(r"")
1781         report = black.Report()
1782         gitignore = PathSpec.from_lines(
1783             "gitwildmatch", ["exclude/", ".definitely_exclude"]
1784         )
1785         sources: List[Path] = []
1786         expected = [
1787             Path(path / "b/dont_exclude/a.py"),
1788             Path(path / "b/dont_exclude/a.pyi"),
1789         ]
1790         this_abs = THIS_DIR.resolve()
1791         sources.extend(
1792             black.gen_python_files(
1793                 path.iterdir(),
1794                 this_abs,
1795                 include,
1796                 exclude,
1797                 None,
1798                 None,
1799                 report,
1800                 gitignore,
1801                 verbose=False,
1802                 quiet=False,
1803             )
1804         )
1805         assert sorted(expected) == sorted(sources)
1806
1807     def test_nested_gitignore(self) -> None:
1808         path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1809         include = re.compile(r"\.pyi?$")
1810         exclude = re.compile(r"")
1811         root_gitignore = black.files.get_gitignore(path)
1812         report = black.Report()
1813         expected: List[Path] = [
1814             Path(path / "x.py"),
1815             Path(path / "root/b.py"),
1816             Path(path / "root/c.py"),
1817             Path(path / "root/child/c.py"),
1818         ]
1819         this_abs = THIS_DIR.resolve()
1820         sources = list(
1821             black.gen_python_files(
1822                 path.iterdir(),
1823                 this_abs,
1824                 include,
1825                 exclude,
1826                 None,
1827                 None,
1828                 report,
1829                 root_gitignore,
1830                 verbose=False,
1831                 quiet=False,
1832             )
1833         )
1834         assert sorted(expected) == sorted(sources)
1835
1836     def test_invalid_gitignore(self) -> None:
1837         path = THIS_DIR / "data" / "invalid_gitignore_tests"
1838         empty_config = path / "pyproject.toml"
1839         result = BlackRunner().invoke(
1840             black.main, ["--verbose", "--config", str(empty_config), str(path)]
1841         )
1842         assert result.exit_code == 1
1843         assert result.stderr_bytes is not None
1844
1845         gitignore = path / ".gitignore"
1846         assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1847
1848     def test_invalid_nested_gitignore(self) -> None:
1849         path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1850         empty_config = path / "pyproject.toml"
1851         result = BlackRunner().invoke(
1852             black.main, ["--verbose", "--config", str(empty_config), str(path)]
1853         )
1854         assert result.exit_code == 1
1855         assert result.stderr_bytes is not None
1856
1857         gitignore = path / "a" / ".gitignore"
1858         assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1859
1860     def test_empty_include(self) -> None:
1861         path = DATA_DIR / "include_exclude_tests"
1862         src = [path]
1863         expected = [
1864             Path(path / "b/exclude/a.pie"),
1865             Path(path / "b/exclude/a.py"),
1866             Path(path / "b/exclude/a.pyi"),
1867             Path(path / "b/dont_exclude/a.pie"),
1868             Path(path / "b/dont_exclude/a.py"),
1869             Path(path / "b/dont_exclude/a.pyi"),
1870             Path(path / "b/.definitely_exclude/a.pie"),
1871             Path(path / "b/.definitely_exclude/a.py"),
1872             Path(path / "b/.definitely_exclude/a.pyi"),
1873             Path(path / ".gitignore"),
1874             Path(path / "pyproject.toml"),
1875         ]
1876         # Setting exclude explicitly to an empty string to block .gitignore usage.
1877         assert_collected_sources(src, expected, include="", exclude="")
1878
1879     def test_extend_exclude(self) -> None:
1880         path = DATA_DIR / "include_exclude_tests"
1881         src = [path]
1882         expected = [
1883             Path(path / "b/exclude/a.py"),
1884             Path(path / "b/dont_exclude/a.py"),
1885         ]
1886         assert_collected_sources(
1887             src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1888         )
1889
1890     def test_symlink_out_of_root_directory(self) -> None:
1891         path = MagicMock()
1892         root = THIS_DIR.resolve()
1893         child = MagicMock()
1894         include = re.compile(black.DEFAULT_INCLUDES)
1895         exclude = re.compile(black.DEFAULT_EXCLUDES)
1896         report = black.Report()
1897         gitignore = PathSpec.from_lines("gitwildmatch", [])
1898         # `child` should behave like a symlink which resolved path is clearly
1899         # outside of the `root` directory.
1900         path.iterdir.return_value = [child]
1901         child.resolve.return_value = Path("/a/b/c")
1902         child.as_posix.return_value = "/a/b/c"
1903         child.is_symlink.return_value = True
1904         try:
1905             list(
1906                 black.gen_python_files(
1907                     path.iterdir(),
1908                     root,
1909                     include,
1910                     exclude,
1911                     None,
1912                     None,
1913                     report,
1914                     gitignore,
1915                     verbose=False,
1916                     quiet=False,
1917                 )
1918             )
1919         except ValueError as ve:
1920             pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1921         path.iterdir.assert_called_once()
1922         child.resolve.assert_called_once()
1923         child.is_symlink.assert_called_once()
1924         # `child` should behave like a strange file which resolved path is clearly
1925         # outside of the `root` directory.
1926         child.is_symlink.return_value = False
1927         with pytest.raises(ValueError):
1928             list(
1929                 black.gen_python_files(
1930                     path.iterdir(),
1931                     root,
1932                     include,
1933                     exclude,
1934                     None,
1935                     None,
1936                     report,
1937                     gitignore,
1938                     verbose=False,
1939                     quiet=False,
1940                 )
1941             )
1942         path.iterdir.assert_called()
1943         assert path.iterdir.call_count == 2
1944         child.resolve.assert_called()
1945         assert child.resolve.call_count == 2
1946         child.is_symlink.assert_called()
1947         assert child.is_symlink.call_count == 2
1948
1949     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1950     def test_get_sources_with_stdin(self) -> None:
1951         src = ["-"]
1952         expected = ["-"]
1953         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1954
1955     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1956     def test_get_sources_with_stdin_filename(self) -> None:
1957         src = ["-"]
1958         stdin_filename = str(THIS_DIR / "data/collections.py")
1959         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1960         assert_collected_sources(
1961             src,
1962             expected,
1963             exclude=r"/exclude/a\.py",
1964             stdin_filename=stdin_filename,
1965         )
1966
1967     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1968     def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1969         # Exclude shouldn't exclude stdin_filename since it is mimicking the
1970         # file being passed directly. This is the same as
1971         # test_exclude_for_issue_1572
1972         path = DATA_DIR / "include_exclude_tests"
1973         src = ["-"]
1974         stdin_filename = str(path / "b/exclude/a.py")
1975         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1976         assert_collected_sources(
1977             src,
1978             expected,
1979             exclude=r"/exclude/|a\.py",
1980             stdin_filename=stdin_filename,
1981         )
1982
1983     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1984     def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1985         # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1986         # file being passed directly. This is the same as
1987         # test_exclude_for_issue_1572
1988         src = ["-"]
1989         path = THIS_DIR / "data" / "include_exclude_tests"
1990         stdin_filename = str(path / "b/exclude/a.py")
1991         expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1992         assert_collected_sources(
1993             src,
1994             expected,
1995             extend_exclude=r"/exclude/|a\.py",
1996             stdin_filename=stdin_filename,
1997         )
1998
1999     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2000     def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2001         # Force exclude should exclude the file when passing it through
2002         # stdin_filename
2003         path = THIS_DIR / "data" / "include_exclude_tests"
2004         stdin_filename = str(path / "b/exclude/a.py")
2005         assert_collected_sources(
2006             src=["-"],
2007             expected=[],
2008             force_exclude=r"/exclude/|a\.py",
2009             stdin_filename=stdin_filename,
2010         )
2011
2012
2013 with open(black.__file__, "r", encoding="utf-8") as _bf:
2014     black_source_lines = _bf.readlines()
2015
2016
2017 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2018     """Show function calls `from black/__init__.py` as they happen.
2019
2020     Register this with `sys.settrace()` in a test you're debugging.
2021     """
2022     if event != "call":
2023         return tracefunc
2024
2025     stack = len(inspect.stack()) - 19
2026     stack *= 2
2027     filename = frame.f_code.co_filename
2028     lineno = frame.f_lineno
2029     func_sig_lineno = lineno - 1
2030     funcname = black_source_lines[func_sig_lineno].strip()
2031     while funcname.startswith("@"):
2032         func_sig_lineno += 1
2033         funcname = black_source_lines[func_sig_lineno].strip()
2034     if "black/__init__.py" in filename:
2035         print(f"{' ' * stack}{lineno}:{funcname}")
2036     return tracefunc