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

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