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

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