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

9c3cc64387b1d91bdc3238024a7e6d94d67aaa5c
[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(), this_abs, include, exclude, None, report, gitignore
1350             )
1351         )
1352         self.assertEqual(sorted(expected), sorted(sources))
1353
1354     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1355     def test_exclude_for_issue_1572(self) -> None:
1356         # Exclude shouldn't touch files that were explicitly given to Black through the
1357         # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1358         # https://github.com/psf/black/issues/1572
1359         path = THIS_DIR / "data" / "include_exclude_tests"
1360         include = ""
1361         exclude = r"/exclude/|a\.py"
1362         src = str(path / "b/exclude/a.py")
1363         report = black.Report()
1364         expected = [Path(path / "b/exclude/a.py")]
1365         sources = list(
1366             black.get_sources(
1367                 ctx=FakeContext(),
1368                 src=(src,),
1369                 quiet=True,
1370                 verbose=False,
1371                 include=include,
1372                 exclude=exclude,
1373                 force_exclude=None,
1374                 report=report,
1375                 stdin_filename=None,
1376             )
1377         )
1378         self.assertEqual(sorted(expected), sorted(sources))
1379
1380     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1381     def test_get_sources_with_stdin(self) -> None:
1382         include = ""
1383         exclude = r"/exclude/|a\.py"
1384         src = "-"
1385         report = black.Report()
1386         expected = [Path("-")]
1387         sources = list(
1388             black.get_sources(
1389                 ctx=FakeContext(),
1390                 src=(src,),
1391                 quiet=True,
1392                 verbose=False,
1393                 include=include,
1394                 exclude=exclude,
1395                 force_exclude=None,
1396                 report=report,
1397                 stdin_filename=None,
1398             )
1399         )
1400         self.assertEqual(sorted(expected), sorted(sources))
1401
1402     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1403     def test_get_sources_with_stdin_filename(self) -> None:
1404         include = ""
1405         exclude = r"/exclude/|a\.py"
1406         src = "-"
1407         report = black.Report()
1408         stdin_filename = str(THIS_DIR / "data/collections.py")
1409         expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1410         sources = list(
1411             black.get_sources(
1412                 ctx=FakeContext(),
1413                 src=(src,),
1414                 quiet=True,
1415                 verbose=False,
1416                 include=include,
1417                 exclude=exclude,
1418                 force_exclude=None,
1419                 report=report,
1420                 stdin_filename=stdin_filename,
1421             )
1422         )
1423         self.assertEqual(sorted(expected), sorted(sources))
1424
1425     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1426     def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1427         # Exclude shouldn't exclude stdin_filename since it is mimicing the
1428         # file being passed directly. This is the same as
1429         # test_exclude_for_issue_1572
1430         path = THIS_DIR / "data" / "include_exclude_tests"
1431         include = ""
1432         exclude = r"/exclude/|a\.py"
1433         src = "-"
1434         report = black.Report()
1435         stdin_filename = str(path / "b/exclude/a.py")
1436         expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1437         sources = list(
1438             black.get_sources(
1439                 ctx=FakeContext(),
1440                 src=(src,),
1441                 quiet=True,
1442                 verbose=False,
1443                 include=include,
1444                 exclude=exclude,
1445                 force_exclude=None,
1446                 report=report,
1447                 stdin_filename=stdin_filename,
1448             )
1449         )
1450         self.assertEqual(sorted(expected), sorted(sources))
1451
1452     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1453     def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1454         # Force exclude should exclude the file when passing it through
1455         # stdin_filename
1456         path = THIS_DIR / "data" / "include_exclude_tests"
1457         include = ""
1458         force_exclude = r"/exclude/|a\.py"
1459         src = "-"
1460         report = black.Report()
1461         stdin_filename = str(path / "b/exclude/a.py")
1462         sources = list(
1463             black.get_sources(
1464                 ctx=FakeContext(),
1465                 src=(src,),
1466                 quiet=True,
1467                 verbose=False,
1468                 include=include,
1469                 exclude="",
1470                 force_exclude=force_exclude,
1471                 report=report,
1472                 stdin_filename=stdin_filename,
1473             )
1474         )
1475         self.assertEqual([], sorted(sources))
1476
1477     def test_reformat_one_with_stdin(self) -> None:
1478         with patch(
1479             "black.format_stdin_to_stdout",
1480             return_value=lambda *args, **kwargs: black.Changed.YES,
1481         ) as fsts:
1482             report = MagicMock()
1483             path = Path("-")
1484             black.reformat_one(
1485                 path,
1486                 fast=True,
1487                 write_back=black.WriteBack.YES,
1488                 mode=DEFAULT_MODE,
1489                 report=report,
1490             )
1491             fsts.assert_called_once()
1492             report.done.assert_called_with(path, black.Changed.YES)
1493
1494     def test_reformat_one_with_stdin_filename(self) -> None:
1495         with patch(
1496             "black.format_stdin_to_stdout",
1497             return_value=lambda *args, **kwargs: black.Changed.YES,
1498         ) as fsts:
1499             report = MagicMock()
1500             p = "foo.py"
1501             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1502             expected = Path(p)
1503             black.reformat_one(
1504                 path,
1505                 fast=True,
1506                 write_back=black.WriteBack.YES,
1507                 mode=DEFAULT_MODE,
1508                 report=report,
1509             )
1510             fsts.assert_called_once()
1511             # __BLACK_STDIN_FILENAME__ should have been striped
1512             report.done.assert_called_with(expected, black.Changed.YES)
1513
1514     def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1515         with patch(
1516             "black.format_stdin_to_stdout",
1517             return_value=lambda *args, **kwargs: black.Changed.YES,
1518         ) as fsts:
1519             report = MagicMock()
1520             # Even with an existing file, since we are forcing stdin, black
1521             # should output to stdout and not modify the file inplace
1522             p = Path(str(THIS_DIR / "data/collections.py"))
1523             # Make sure is_file actually returns True
1524             self.assertTrue(p.is_file())
1525             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1526             expected = Path(p)
1527             black.reformat_one(
1528                 path,
1529                 fast=True,
1530                 write_back=black.WriteBack.YES,
1531                 mode=DEFAULT_MODE,
1532                 report=report,
1533             )
1534             fsts.assert_called_once()
1535             # __BLACK_STDIN_FILENAME__ should have been striped
1536             report.done.assert_called_with(expected, black.Changed.YES)
1537
1538     def test_gitignore_exclude(self) -> None:
1539         path = THIS_DIR / "data" / "include_exclude_tests"
1540         include = re.compile(r"\.pyi?$")
1541         exclude = re.compile(r"")
1542         report = black.Report()
1543         gitignore = PathSpec.from_lines(
1544             "gitwildmatch", ["exclude/", ".definitely_exclude"]
1545         )
1546         sources: List[Path] = []
1547         expected = [
1548             Path(path / "b/dont_exclude/a.py"),
1549             Path(path / "b/dont_exclude/a.pyi"),
1550         ]
1551         this_abs = THIS_DIR.resolve()
1552         sources.extend(
1553             black.gen_python_files(
1554                 path.iterdir(), this_abs, include, exclude, None, report, gitignore
1555             )
1556         )
1557         self.assertEqual(sorted(expected), sorted(sources))
1558
1559     def test_empty_include(self) -> None:
1560         path = THIS_DIR / "data" / "include_exclude_tests"
1561         report = black.Report()
1562         gitignore = PathSpec.from_lines("gitwildmatch", [])
1563         empty = re.compile(r"")
1564         sources: List[Path] = []
1565         expected = [
1566             Path(path / "b/exclude/a.pie"),
1567             Path(path / "b/exclude/a.py"),
1568             Path(path / "b/exclude/a.pyi"),
1569             Path(path / "b/dont_exclude/a.pie"),
1570             Path(path / "b/dont_exclude/a.py"),
1571             Path(path / "b/dont_exclude/a.pyi"),
1572             Path(path / "b/.definitely_exclude/a.pie"),
1573             Path(path / "b/.definitely_exclude/a.py"),
1574             Path(path / "b/.definitely_exclude/a.pyi"),
1575         ]
1576         this_abs = THIS_DIR.resolve()
1577         sources.extend(
1578             black.gen_python_files(
1579                 path.iterdir(),
1580                 this_abs,
1581                 empty,
1582                 re.compile(black.DEFAULT_EXCLUDES),
1583                 None,
1584                 report,
1585                 gitignore,
1586             )
1587         )
1588         self.assertEqual(sorted(expected), sorted(sources))
1589
1590     def test_empty_exclude(self) -> None:
1591         path = THIS_DIR / "data" / "include_exclude_tests"
1592         report = black.Report()
1593         gitignore = PathSpec.from_lines("gitwildmatch", [])
1594         empty = re.compile(r"")
1595         sources: List[Path] = []
1596         expected = [
1597             Path(path / "b/dont_exclude/a.py"),
1598             Path(path / "b/dont_exclude/a.pyi"),
1599             Path(path / "b/exclude/a.py"),
1600             Path(path / "b/exclude/a.pyi"),
1601             Path(path / "b/.definitely_exclude/a.py"),
1602             Path(path / "b/.definitely_exclude/a.pyi"),
1603         ]
1604         this_abs = THIS_DIR.resolve()
1605         sources.extend(
1606             black.gen_python_files(
1607                 path.iterdir(),
1608                 this_abs,
1609                 re.compile(black.DEFAULT_INCLUDES),
1610                 empty,
1611                 None,
1612                 report,
1613                 gitignore,
1614             )
1615         )
1616         self.assertEqual(sorted(expected), sorted(sources))
1617
1618     def test_invalid_include_exclude(self) -> None:
1619         for option in ["--include", "--exclude"]:
1620             self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1621
1622     def test_preserves_line_endings(self) -> None:
1623         with TemporaryDirectory() as workspace:
1624             test_file = Path(workspace) / "test.py"
1625             for nl in ["\n", "\r\n"]:
1626                 contents = nl.join(["def f(  ):", "    pass"])
1627                 test_file.write_bytes(contents.encode())
1628                 ff(test_file, write_back=black.WriteBack.YES)
1629                 updated_contents: bytes = test_file.read_bytes()
1630                 self.assertIn(nl.encode(), updated_contents)
1631                 if nl == "\n":
1632                     self.assertNotIn(b"\r\n", updated_contents)
1633
1634     def test_preserves_line_endings_via_stdin(self) -> None:
1635         for nl in ["\n", "\r\n"]:
1636             contents = nl.join(["def f(  ):", "    pass"])
1637             runner = BlackRunner()
1638             result = runner.invoke(
1639                 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1640             )
1641             self.assertEqual(result.exit_code, 0)
1642             output = runner.stdout_bytes
1643             self.assertIn(nl.encode("utf8"), output)
1644             if nl == "\n":
1645                 self.assertNotIn(b"\r\n", output)
1646
1647     def test_assert_equivalent_different_asts(self) -> None:
1648         with self.assertRaises(AssertionError):
1649             black.assert_equivalent("{}", "None")
1650
1651     def test_symlink_out_of_root_directory(self) -> None:
1652         path = MagicMock()
1653         root = THIS_DIR.resolve()
1654         child = MagicMock()
1655         include = re.compile(black.DEFAULT_INCLUDES)
1656         exclude = re.compile(black.DEFAULT_EXCLUDES)
1657         report = black.Report()
1658         gitignore = PathSpec.from_lines("gitwildmatch", [])
1659         # `child` should behave like a symlink which resolved path is clearly
1660         # outside of the `root` directory.
1661         path.iterdir.return_value = [child]
1662         child.resolve.return_value = Path("/a/b/c")
1663         child.as_posix.return_value = "/a/b/c"
1664         child.is_symlink.return_value = True
1665         try:
1666             list(
1667                 black.gen_python_files(
1668                     path.iterdir(), root, include, exclude, None, report, gitignore
1669                 )
1670             )
1671         except ValueError as ve:
1672             self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1673         path.iterdir.assert_called_once()
1674         child.resolve.assert_called_once()
1675         child.is_symlink.assert_called_once()
1676         # `child` should behave like a strange file which resolved path is clearly
1677         # outside of the `root` directory.
1678         child.is_symlink.return_value = False
1679         with self.assertRaises(ValueError):
1680             list(
1681                 black.gen_python_files(
1682                     path.iterdir(), root, include, exclude, None, report, gitignore
1683                 )
1684             )
1685         path.iterdir.assert_called()
1686         self.assertEqual(path.iterdir.call_count, 2)
1687         child.resolve.assert_called()
1688         self.assertEqual(child.resolve.call_count, 2)
1689         child.is_symlink.assert_called()
1690         self.assertEqual(child.is_symlink.call_count, 2)
1691
1692     def test_shhh_click(self) -> None:
1693         try:
1694             from click import _unicodefun  # type: ignore
1695         except ModuleNotFoundError:
1696             self.skipTest("Incompatible Click version")
1697         if not hasattr(_unicodefun, "_verify_python3_env"):
1698             self.skipTest("Incompatible Click version")
1699         # First, let's see if Click is crashing with a preferred ASCII charset.
1700         with patch("locale.getpreferredencoding") as gpe:
1701             gpe.return_value = "ASCII"
1702             with self.assertRaises(RuntimeError):
1703                 _unicodefun._verify_python3_env()
1704         # Now, let's silence Click...
1705         black.patch_click()
1706         # ...and confirm it's silent.
1707         with patch("locale.getpreferredencoding") as gpe:
1708             gpe.return_value = "ASCII"
1709             try:
1710                 _unicodefun._verify_python3_env()
1711             except RuntimeError as re:
1712                 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1713
1714     def test_root_logger_not_used_directly(self) -> None:
1715         def fail(*args: Any, **kwargs: Any) -> None:
1716             self.fail("Record created with root logger")
1717
1718         with patch.multiple(
1719             logging.root,
1720             debug=fail,
1721             info=fail,
1722             warning=fail,
1723             error=fail,
1724             critical=fail,
1725             log=fail,
1726         ):
1727             ff(THIS_FILE)
1728
1729     def test_invalid_config_return_code(self) -> None:
1730         tmp_file = Path(black.dump_to_file())
1731         try:
1732             tmp_config = Path(black.dump_to_file())
1733             tmp_config.unlink()
1734             args = ["--config", str(tmp_config), str(tmp_file)]
1735             self.invokeBlack(args, exit_code=2, ignore_config=False)
1736         finally:
1737             tmp_file.unlink()
1738
1739     def test_parse_pyproject_toml(self) -> None:
1740         test_toml_file = THIS_DIR / "test.toml"
1741         config = black.parse_pyproject_toml(str(test_toml_file))
1742         self.assertEqual(config["verbose"], 1)
1743         self.assertEqual(config["check"], "no")
1744         self.assertEqual(config["diff"], "y")
1745         self.assertEqual(config["color"], True)
1746         self.assertEqual(config["line_length"], 79)
1747         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1748         self.assertEqual(config["exclude"], r"\.pyi?$")
1749         self.assertEqual(config["include"], r"\.py?$")
1750
1751     def test_read_pyproject_toml(self) -> None:
1752         test_toml_file = THIS_DIR / "test.toml"
1753         fake_ctx = FakeContext()
1754         black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1755         config = fake_ctx.default_map
1756         self.assertEqual(config["verbose"], "1")
1757         self.assertEqual(config["check"], "no")
1758         self.assertEqual(config["diff"], "y")
1759         self.assertEqual(config["color"], "True")
1760         self.assertEqual(config["line_length"], "79")
1761         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1762         self.assertEqual(config["exclude"], r"\.pyi?$")
1763         self.assertEqual(config["include"], r"\.py?$")
1764
1765     def test_find_project_root(self) -> None:
1766         with TemporaryDirectory() as workspace:
1767             root = Path(workspace)
1768             test_dir = root / "test"
1769             test_dir.mkdir()
1770
1771             src_dir = root / "src"
1772             src_dir.mkdir()
1773
1774             root_pyproject = root / "pyproject.toml"
1775             root_pyproject.touch()
1776             src_pyproject = src_dir / "pyproject.toml"
1777             src_pyproject.touch()
1778             src_python = src_dir / "foo.py"
1779             src_python.touch()
1780
1781             self.assertEqual(
1782                 black.find_project_root((src_dir, test_dir)), root.resolve()
1783             )
1784             self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1785             self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1786
1787     def test_bpo_33660_workaround(self) -> None:
1788         if system() == "Windows":
1789             return
1790
1791         # https://bugs.python.org/issue33660
1792
1793         old_cwd = Path.cwd()
1794         try:
1795             root = Path("/")
1796             os.chdir(str(root))
1797             path = Path("workspace") / "project"
1798             report = black.Report(verbose=True)
1799             normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1800             self.assertEqual(normalized_path, "workspace/project")
1801         finally:
1802             os.chdir(str(old_cwd))
1803
1804     def test_newline_comment_interaction(self) -> None:
1805         source = "class A:\\\r\n# type: ignore\n pass\n"
1806         output = black.format_str(source, mode=DEFAULT_MODE)
1807         black.assert_stable(source, output, mode=DEFAULT_MODE)
1808
1809     def test_bpo_2142_workaround(self) -> None:
1810
1811         # https://bugs.python.org/issue2142
1812
1813         source, _ = read_data("missing_final_newline.py")
1814         # read_data adds a trailing newline
1815         source = source.rstrip()
1816         expected, _ = read_data("missing_final_newline.diff")
1817         tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1818         diff_header = re.compile(
1819             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1820             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1821         )
1822         try:
1823             result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1824             self.assertEqual(result.exit_code, 0)
1825         finally:
1826             os.unlink(tmp_file)
1827         actual = result.output
1828         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1829         self.assertEqual(actual, expected)
1830
1831
1832 with open(black.__file__, "r", encoding="utf-8") as _bf:
1833     black_source_lines = _bf.readlines()
1834
1835
1836 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
1837     """Show function calls `from black/__init__.py` as they happen.
1838
1839     Register this with `sys.settrace()` in a test you're debugging.
1840     """
1841     if event != "call":
1842         return tracefunc
1843
1844     stack = len(inspect.stack()) - 19
1845     stack *= 2
1846     filename = frame.f_code.co_filename
1847     lineno = frame.f_lineno
1848     func_sig_lineno = lineno - 1
1849     funcname = black_source_lines[func_sig_lineno].strip()
1850     while funcname.startswith("@"):
1851         func_sig_lineno += 1
1852         funcname = black_source_lines[func_sig_lineno].strip()
1853     if "black/__init__.py" in filename:
1854         print(f"{' ' * stack}{lineno}:{funcname}")
1855     return tracefunc
1856
1857
1858 if __name__ == "__main__":
1859     unittest.main(module="test_black")