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

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