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

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