X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/2dfa69bb7fc1d310902ae565271d595d307f166b..0721f11731b64b1b134f3cb7c227a6477044e8fa:/tests/test_black.py?ds=inline diff --git a/tests/test_black.py b/tests/test_black.py index 191f4b8..6fb01f7 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 import asyncio +import logging from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from functools import partial from io import BytesIO, TextIOWrapper import os from pathlib import Path -import re +import regex as re import sys from tempfile import TemporaryDirectory -from typing import Any, BinaryIO, Generator, List, Tuple, Iterator +from typing import Any, BinaryIO, Generator, List, Tuple, Iterator, TypeVar import unittest from unittest.mock import patch, MagicMock @@ -17,14 +18,30 @@ from click import unstyle from click.testing import CliRunner import black +from black import Feature, TargetVersion +try: + import blackd + from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop + from aiohttp import web +except ImportError: + has_blackd_deps = False +else: + has_blackd_deps = True -ll = 88 -ff = partial(black.format_file_in_place, line_length=ll, fast=True) -fs = partial(black.format_str, line_length=ll) +from pathspec import PathSpec + +ff = partial(black.format_file_in_place, mode=black.FileMode(), fast=True) +fs = partial(black.format_str, mode=black.FileMode()) THIS_FILE = Path(__file__) THIS_DIR = THIS_FILE.parent +DETERMINISTIC_HEADER = "[Deterministic header]" EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" +PY36_ARGS = [ + f"--target-version={version.name.lower()}" for version in black.PY36_VERSIONS +] +T = TypeVar("T") +R = TypeVar("R") def dump_to_stderr(*output: str) -> str: @@ -65,18 +82,26 @@ def cache_dir(exists: bool = True) -> Iterator[Path]: @contextmanager -def event_loop(close: bool) -> Iterator[None]: +def event_loop() -> Iterator[None]: policy = asyncio.get_event_loop_policy() - old_loop = policy.get_event_loop() loop = policy.new_event_loop() asyncio.set_event_loop(loop) try: yield finally: - policy.set_event_loop(old_loop) - if close: - loop.close() + loop.close() + + +@contextmanager +def skip_if_exception(e: str) -> Iterator[None]: + try: + yield + except Exception as exc: + if exc.__class__.__name__ == e: + unittest.skip(f"Encountered expected exception {exc}, skipping") + else: + raise class BlackRunner(CliRunner): @@ -84,8 +109,11 @@ class BlackRunner(CliRunner): This is a hack that can be removed once we depend on Click 7.x""" - def __init__(self, stderrbuf: BinaryIO) -> None: - self.stderrbuf = stderrbuf + def __init__(self) -> None: + self.stderrbuf = BytesIO() + self.stdoutbuf = BytesIO() + self.stdout_bytes = b"" + self.stderr_bytes = b"" super().__init__() @contextmanager @@ -96,6 +124,8 @@ class BlackRunner(CliRunner): sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset) yield output finally: + self.stdout_bytes = sys.stdout.buffer.getvalue() # type: ignore + self.stderr_bytes = sys.stderr.buffer.getvalue() # type: ignore sys.stderr = hold_stderr @@ -121,13 +151,41 @@ class BlackTestCase(unittest.TestCase): black.err(str(ve)) self.assertEqual(expected, actual) + def invokeBlack( + self, args: List[str], exit_code: int = 0, ignore_config: bool = True + ) -> None: + runner = BlackRunner() + if ignore_config: + args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args] + result = runner.invoke(black.main, args) + self.assertEqual( + result.exit_code, + exit_code, + msg=( + f"Failed with args: {args}\n" + f"stdout: {runner.stdout_bytes.decode()!r}\n" + f"stderr: {runner.stderr_bytes.decode()!r}\n" + f"exception: {result.exception}" + ), + ) + + @patch("black.dump_to_file", dump_to_stderr) + def checkSourceFile(self, name: str) -> None: + path = THIS_DIR.parent / name + source, expected = read_data(str(path), data=False) + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + self.assertFalse(ff(path)) + @patch("black.dump_to_file", dump_to_stderr) def test_empty(self) -> None: source = expected = "" actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) def test_empty_ff(self) -> None: expected = "" @@ -140,59 +198,101 @@ class BlackTestCase(unittest.TestCase): os.unlink(tmp_file) self.assertFormatEqual(expected, actual) - @patch("black.dump_to_file", dump_to_stderr) def test_self(self) -> None: - source, expected = read_data("test_black", data=False) - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) - self.assertFalse(ff(THIS_FILE)) + self.checkSourceFile("tests/test_black.py") - @patch("black.dump_to_file", dump_to_stderr) def test_black(self) -> None: - source, expected = read_data("../black", data=False) - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) - self.assertFalse(ff(THIS_DIR / ".." / "black.py")) + self.checkSourceFile("black.py") + + def test_pygram(self) -> None: + self.checkSourceFile("blib2to3/pygram.py") + + def test_pytree(self) -> None: + self.checkSourceFile("blib2to3/pytree.py") + + def test_conv(self) -> None: + self.checkSourceFile("blib2to3/pgen2/conv.py") + + def test_driver(self) -> None: + self.checkSourceFile("blib2to3/pgen2/driver.py") + + def test_grammar(self) -> None: + self.checkSourceFile("blib2to3/pgen2/grammar.py") + + def test_literals(self) -> None: + self.checkSourceFile("blib2to3/pgen2/literals.py") + + def test_parse(self) -> None: + self.checkSourceFile("blib2to3/pgen2/parse.py") + + def test_pgen(self) -> None: + self.checkSourceFile("blib2to3/pgen2/pgen.py") + + def test_tokenize(self) -> None: + self.checkSourceFile("blib2to3/pgen2/tokenize.py") + + def test_token(self) -> None: + self.checkSourceFile("blib2to3/pgen2/token.py") + + def test_setup(self) -> None: + self.checkSourceFile("setup.py") def test_piping(self) -> None: source, expected = read_data("../black", data=False) - stderrbuf = BytesIO() - result = BlackRunner(stderrbuf).invoke( - black.main, ["-", "--fast", f"--line-length={ll}"], input=source + result = BlackRunner().invoke( + black.main, + ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"], + input=BytesIO(source.encode("utf8")), ) self.assertEqual(result.exit_code, 0) self.assertFormatEqual(expected, result.output) black.assert_equivalent(source, result.output) - black.assert_stable(source, result.output, line_length=ll) + black.assert_stable(source, result.output, black.FileMode()) def test_piping_diff(self) -> None: diff_header = re.compile( - rf"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d " - rf"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + rf"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d " + rf"\+\d\d\d\d" ) source, _ = read_data("expression.py") expected, _ = read_data("expression.diff") config = THIS_DIR / "data" / "empty_pyproject.toml" - stderrbuf = BytesIO() - args = ["-", "--fast", f"--line-length={ll}", "--diff", f"--config={config}"] - result = BlackRunner(stderrbuf).invoke(black.main, args, input=source) + args = [ + "-", + "--fast", + f"--line-length={black.DEFAULT_LINE_LENGTH}", + "--diff", + f"--config={config}", + ] + result = BlackRunner().invoke( + black.main, args, input=BytesIO(source.encode("utf8")) + ) self.assertEqual(result.exit_code, 0) - actual = diff_header.sub("[Deterministic header]", result.output) + actual = diff_header.sub(DETERMINISTIC_HEADER, result.output) actual = actual.rstrip() + "\n" # the diff output has a trailing space self.assertEqual(expected, actual) - @patch("black.dump_to_file", dump_to_stderr) - def test_setup(self) -> None: - source, expected = read_data("../setup", data=False) - actual = fs(source) - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) - self.assertFalse(ff(THIS_DIR / ".." / "setup.py")) + def test_piping_diff_with_color(self) -> None: + source, _ = read_data("expression.py") + config = THIS_DIR / "data" / "empty_pyproject.toml" + args = [ + "-", + "--fast", + f"--line-length={black.DEFAULT_LINE_LENGTH}", + "--diff", + "--color", + f"--config={config}", + ] + result = BlackRunner().invoke( + black.main, args, input=BytesIO(source.encode("utf8")) + ) + actual = result.output + # Again, the contents are checked in a different test, so only look for colors. + self.assertIn("\033[1;37m", actual) + self.assertIn("\033[36m", actual) + self.assertIn("\033[32m", actual) + self.assertIn("\033[31m", actual) + self.assertIn("\033[0m", actual) @patch("black.dump_to_file", dump_to_stderr) def test_function(self) -> None: @@ -200,7 +300,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_function2(self) -> None: @@ -208,7 +308,15 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_function_trailing_comma(self) -> None: + source, expected = read_data("function_trailing_comma") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_expression(self) -> None: @@ -216,7 +324,24 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_pep_572(self) -> None: + source, expected = read_data("pep_572") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_stable(source, actual, black.FileMode()) + if sys.version_info >= (3, 8): + black.assert_equivalent(source, actual) + + def test_pep_572_version_detection(self) -> None: + source, _ = read_data("pep_572") + root = black.lib2to3_parse(source) + features = black.get_features_used(root) + self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features) + versions = black.detect_target_versions(root) + self.assertIn(black.TargetVersion.PY38, versions) def test_expression_ff(self) -> None: source, expected = read_data("expression") @@ -230,7 +355,7 @@ class BlackTestCase(unittest.TestCase): self.assertFormatEqual(expected, actual) with patch("black.dump_to_file", dump_to_stderr): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) def test_expression_diff(self) -> None: source, _ = read_data("expression.py") @@ -238,35 +363,68 @@ class BlackTestCase(unittest.TestCase): tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " - rf"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" ) - stderrbuf = BytesIO() try: - result = BlackRunner(stderrbuf).invoke( - black.main, ["--diff", str(tmp_file)] - ) + result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)]) self.assertEqual(result.exit_code, 0) finally: os.unlink(tmp_file) actual = result.output - actual = diff_header.sub("[Deterministic header]", actual) + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) actual = actual.rstrip() + "\n" # the diff output has a trailing space if expected != actual: dump = black.dump_to_file(actual) msg = ( - f"Expected diff isn't equal to the actual. If you made changes " - f"to expression.py and this is an anticipated difference, " - f"overwrite tests/expression.diff with {dump}" + "Expected diff isn't equal to the actual. If you made changes to" + " expression.py and this is an anticipated difference, overwrite" + f" tests/data/expression.diff with {dump}" ) self.assertEqual(expected, actual, msg) + def test_expression_diff_with_color(self) -> None: + source, _ = read_data("expression.py") + expected, _ = read_data("expression.diff") + tmp_file = Path(black.dump_to_file(source)) + try: + result = BlackRunner().invoke( + black.main, ["--diff", "--color", str(tmp_file)] + ) + finally: + os.unlink(tmp_file) + actual = result.output + # We check the contents of the diff in `test_expression_diff`. All + # we need to check here is that color codes exist in the result. + self.assertIn("\033[1;37m", actual) + self.assertIn("\033[36m", actual) + self.assertIn("\033[32m", actual) + self.assertIn("\033[31m", actual) + self.assertIn("\033[0m", actual) + @patch("black.dump_to_file", dump_to_stderr) def test_fstring(self) -> None: source, expected = read_data("fstring") actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_pep_570(self) -> None: + source, expected = read_data("pep_570") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_stable(source, actual, black.FileMode()) + if sys.version_info >= (3, 8): + black.assert_equivalent(source, actual) + + def test_detect_pos_only_arguments(self) -> None: + source, _ = read_data("pep_570") + root = black.lib2to3_parse(source) + features = black.get_features_used(root) + self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features) + versions = black.detect_target_versions(root) + self.assertIn(black.TargetVersion.PY38, versions) @patch("black.dump_to_file", dump_to_stderr) def test_string_quotes(self) -> None: @@ -274,12 +432,46 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) - mode = black.FileMode.NO_STRING_NORMALIZATION + black.assert_stable(source, actual, black.FileMode()) + mode = black.FileMode(string_normalization=False) not_normalized = fs(source, mode=mode) - self.assertFormatEqual(source, not_normalized) + self.assertFormatEqual(source.replace("\\\n", ""), not_normalized) black.assert_equivalent(source, not_normalized) - black.assert_stable(source, not_normalized, line_length=ll, mode=mode) + black.assert_stable(source, not_normalized, mode=mode) + + @patch("black.dump_to_file", dump_to_stderr) + def test_docstring(self) -> None: + source, expected = read_data("docstring") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + def test_long_strings(self) -> None: + """Tests for splitting long strings.""" + source, expected = read_data("long_strings") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_long_strings__edge_case(self) -> None: + """Edge-case tests for splitting long strings.""" + source, expected = read_data("long_strings__edge_case") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_long_strings__regression(self) -> None: + """Regression tests for splitting long strings.""" + source, expected = read_data("long_strings__regression") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_slices(self) -> None: @@ -287,7 +479,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_comments(self) -> None: @@ -295,7 +487,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_comments2(self) -> None: @@ -303,7 +495,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_comments3(self) -> None: @@ -311,7 +503,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_comments4(self) -> None: @@ -319,7 +511,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_comments5(self) -> None: @@ -327,7 +519,31 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_comments6(self) -> None: + source, expected = read_data("comments6") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_comments7(self) -> None: + source, expected = read_data("comments7") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_comment_after_escaped_newline(self) -> None: + source, expected = read_data("comment_after_escaped_newline") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_cantfit(self) -> None: @@ -335,7 +551,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_import_spacing(self) -> None: @@ -343,7 +559,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_composition(self) -> None: @@ -351,7 +567,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_empty_lines(self) -> None: @@ -359,7 +575,15 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_remove_parens(self) -> None: + source, expected = read_data("remove_parens") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_string_prefixes(self) -> None: @@ -367,30 +591,105 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_numeric_literals(self) -> None: + source, expected = read_data("numeric_literals") + mode = black.FileMode(target_versions=black.PY36_VERSIONS) + actual = fs(source, mode=mode) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, mode) + + @patch("black.dump_to_file", dump_to_stderr) + def test_numeric_literals_ignoring_underscores(self) -> None: + source, expected = read_data("numeric_literals_skip_underscores") + mode = black.FileMode(target_versions=black.PY36_VERSIONS) + actual = fs(source, mode=mode) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, mode) + + @patch("black.dump_to_file", dump_to_stderr) + def test_numeric_literals_py2(self) -> None: + source, expected = read_data("numeric_literals_py2") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_python2(self) -> None: source, expected = read_data("python2") actual = fs(source) self.assertFormatEqual(expected, actual) - # black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_python2_print_function(self) -> None: + source, expected = read_data("python2_print_function") + mode = black.FileMode(target_versions={TargetVersion.PY27}) + actual = fs(source, mode=mode) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, mode) @patch("black.dump_to_file", dump_to_stderr) def test_python2_unicode_literals(self) -> None: source, expected = read_data("python2_unicode_literals") actual = fs(source) self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_stub(self) -> None: - mode = black.FileMode.PYI + mode = black.FileMode(is_pyi=True) source, expected = read_data("stub.pyi") actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, line_length=ll, mode=mode) + black.assert_stable(source, actual, mode) + + @patch("black.dump_to_file", dump_to_stderr) + def test_async_as_identifier(self) -> None: + source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve() + source, expected = read_data("async_as_identifier") + actual = fs(source) + self.assertFormatEqual(expected, actual) + major, minor = sys.version_info[:2] + if major < 3 or (major <= 3 and minor < 7): + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + # ensure black can parse this when the target is 3.6 + self.invokeBlack([str(source_path), "--target-version", "py36"]) + # but not on 3.7, because async/await is no longer an identifier + self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123) + + @patch("black.dump_to_file", dump_to_stderr) + def test_python37(self) -> None: + source_path = (THIS_DIR / "data" / "python37.py").resolve() + source, expected = read_data("python37") + actual = fs(source) + self.assertFormatEqual(expected, actual) + major, minor = sys.version_info[:2] + if major > 3 or (major == 3 and minor >= 7): + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + # ensure black can parse this when the target is 3.7 + self.invokeBlack([str(source_path), "--target-version", "py37"]) + # but not on 3.6, because we use async as a reserved keyword + self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123) + + @patch("black.dump_to_file", dump_to_stderr) + def test_python38(self) -> None: + source, expected = read_data("python38") + actual = fs(source) + self.assertFormatEqual(expected, actual) + major, minor = sys.version_info[:2] + if major > 3 or (major == 3 and minor >= 8): + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff(self) -> None: @@ -398,7 +697,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff2(self) -> None: @@ -406,7 +705,23 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_fmtonoff3(self) -> None: + source, expected = read_data("fmtonoff3") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_fmtonoff4(self) -> None: + source, expected = read_data("fmtonoff4") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_remove_empty_parentheses_after_class(self) -> None: @@ -414,7 +729,7 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) def test_new_line_between_class_and_code(self) -> None: @@ -422,7 +737,53 @@ class BlackTestCase(unittest.TestCase): actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_bracket_match(self) -> None: + source, expected = read_data("bracketmatch") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_tuple_assign(self) -> None: + source, expected = read_data("tupleassign") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + @patch("black.dump_to_file", dump_to_stderr) + def test_beginning_backslash(self) -> None: + source, expected = read_data("beginning_backslash") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + + def test_tab_comment_indentation(self) -> None: + contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n" + contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n" + self.assertFormatEqual(contents_spc, fs(contents_spc)) + self.assertFormatEqual(contents_spc, fs(contents_tab)) + + contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n" + contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n" + self.assertFormatEqual(contents_spc, fs(contents_spc)) + self.assertFormatEqual(contents_spc, fs(contents_tab)) + + # mixed tabs and spaces (valid Python 2 code) + contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n" + contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n" + self.assertFormatEqual(contents_spc, fs(contents_spc)) + self.assertFormatEqual(contents_spc, fs(contents_tab)) + + contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n" + contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n" + self.assertFormatEqual(contents_spc, fs(contents_spc)) + self.assertFormatEqual(contents_spc, fs(contents_tab)) def test_report_verbose(self) -> None: report = black.Report(verbose=True) @@ -468,8 +829,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - "1 file reformatted, 2 files left unchanged, " - "1 file failed to reformat.", + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -478,8 +839,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "1 file failed to reformat.", + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -488,8 +849,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -498,8 +859,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(out_lines[-1], "wat ignored: no match") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -508,15 +869,22 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(out_lines[-1], "f4 already well formatted, good job.") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 3 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, " - "2 files would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2 files" + " would fail to reformat.", + ) + report.check = False + report.diff = True + self.assertEqual( + unstyle(str(report)), + "2 files would be reformatted, 3 files would be left unchanged, 2 files" + " would fail to reformat.", ) def test_report_quiet(self) -> None: @@ -558,8 +926,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - "1 file reformatted, 2 files left unchanged, " - "1 file failed to reformat.", + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -567,8 +935,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(len(err_lines), 1) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "1 file failed to reformat.", + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -577,8 +945,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -586,8 +954,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -595,15 +963,22 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 3 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, " - "2 files would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2 files" + " would fail to reformat.", + ) + report.check = False + report.diff = True + self.assertEqual( + unstyle(str(report)), + "2 files would be reformatted, 3 files would be left unchanged, 2 files" + " would fail to reformat.", ) def test_report_normal(self) -> None: @@ -647,8 +1022,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual( unstyle(str(report)), - "1 file reformatted, 2 files left unchanged, " - "1 file failed to reformat.", + "1 file reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f3"), black.Changed.YES) @@ -657,8 +1032,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "1 file failed to reformat.", + "2 files reformatted, 2 files left unchanged, 1 file failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.failed(Path("e2"), "boom") @@ -667,8 +1042,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.path_ignored(Path("wat"), "no match") @@ -676,8 +1051,8 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 2 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.done(Path("f4"), black.Changed.NO) @@ -685,34 +1060,79 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(len(err_lines), 2) self.assertEqual( unstyle(str(report)), - "2 files reformatted, 3 files left unchanged, " - "2 files failed to reformat.", + "2 files reformatted, 3 files left unchanged, 2 files failed to" + " reformat.", ) self.assertEqual(report.return_code, 123) report.check = True self.assertEqual( unstyle(str(report)), - "2 files would be reformatted, 3 files would be left unchanged, " - "2 files would fail to reformat.", + "2 files would be reformatted, 3 files would be left unchanged, 2 files" + " would fail to reformat.", + ) + report.check = False + report.diff = True + self.assertEqual( + unstyle(str(report)), + "2 files would be reformatted, 3 files would be left unchanged, 2 files" + " would fail to reformat.", ) - def test_is_python36(self) -> None: + def test_lib2to3_parse(self) -> None: + with self.assertRaises(black.InvalidInput): + black.lib2to3_parse("invalid syntax") + + straddling = "x + y" + black.lib2to3_parse(straddling) + black.lib2to3_parse(straddling, {TargetVersion.PY27}) + black.lib2to3_parse(straddling, {TargetVersion.PY36}) + black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36}) + + py2_only = "print x" + black.lib2to3_parse(py2_only) + black.lib2to3_parse(py2_only, {TargetVersion.PY27}) + with self.assertRaises(black.InvalidInput): + black.lib2to3_parse(py2_only, {TargetVersion.PY36}) + with self.assertRaises(black.InvalidInput): + black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36}) + + py3_only = "exec(x, end=y)" + black.lib2to3_parse(py3_only) + with self.assertRaises(black.InvalidInput): + black.lib2to3_parse(py3_only, {TargetVersion.PY27}) + black.lib2to3_parse(py3_only, {TargetVersion.PY36}) + black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36}) + + def test_get_features_used(self) -> None: node = black.lib2to3_parse("def f(*, arg): ...\n") - self.assertFalse(black.is_python36(node)) + self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse("def f(*, arg,): ...\n") - self.assertTrue(black.is_python36(node)) + self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF}) + node = black.lib2to3_parse("f(*arg,)\n") + self.assertEqual( + black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL} + ) node = black.lib2to3_parse("def f(*, arg): f'string'\n") - self.assertTrue(black.is_python36(node)) + self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS}) + node = black.lib2to3_parse("123_456\n") + self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES}) + node = black.lib2to3_parse("123456\n") + self.assertEqual(black.get_features_used(node), set()) source, expected = read_data("function") node = black.lib2to3_parse(source) - self.assertTrue(black.is_python36(node)) + expected_features = { + Feature.TRAILING_COMMA_IN_CALL, + Feature.TRAILING_COMMA_IN_DEF, + Feature.F_STRINGS, + } + self.assertEqual(black.get_features_used(node), expected_features) node = black.lib2to3_parse(expected) - self.assertTrue(black.is_python36(node)) + self.assertEqual(black.get_features_used(node), expected_features) source, expected = read_data("expression") node = black.lib2to3_parse(source) - self.assertFalse(black.is_python36(node)) + self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse(expected) - self.assertFalse(black.is_python36(node)) + self.assertEqual(black.get_features_used(node), set()) def test_get_future_imports(self) -> None: node = black.lib2to3_parse("\n") @@ -735,6 +1155,14 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(set(), black.get_future_imports(node)) node = black.lib2to3_parse("from some.module import black\n") self.assertEqual(set(), black.get_future_imports(node)) + node = black.lib2to3_parse( + "from __future__ import unicode_literals as _unicode_literals" + ) + self.assertEqual({"unicode_literals"}, black.get_future_imports(node)) + node = black.lib2to3_parse( + "from __future__ import unicode_literals as _lol, print" + ) + self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node)) def test_debug_visitor(self) -> None: source, _ = read_data("debug_visitor.py") @@ -762,21 +1190,22 @@ class BlackTestCase(unittest.TestCase): def test_format_file_contents(self) -> None: empty = "" + mode = black.FileMode() with self.assertRaises(black.NothingChanged): - black.format_file_contents(empty, line_length=ll, fast=False) + black.format_file_contents(empty, mode=mode, fast=False) just_nl = "\n" with self.assertRaises(black.NothingChanged): - black.format_file_contents(just_nl, line_length=ll, fast=False) - same = "l = [1, 2, 3]\n" + black.format_file_contents(just_nl, mode=mode, fast=False) + same = "j = [1, 2, 3]\n" with self.assertRaises(black.NothingChanged): - black.format_file_contents(same, line_length=ll, fast=False) - different = "l = [1,2,3]" + black.format_file_contents(same, mode=mode, fast=False) + different = "j = [1,2,3]" expected = same - actual = black.format_file_contents(different, line_length=ll, fast=False) + actual = black.format_file_contents(different, mode=mode, fast=False) self.assertEqual(expected, actual) invalid = "return if you can" - with self.assertRaises(ValueError) as e: - black.format_file_contents(invalid, line_length=ll, fast=False) + with self.assertRaises(black.InvalidInput) as e: + black.format_file_contents(invalid, mode=mode, fast=False) self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can") def test_endmarker(self) -> None: @@ -798,7 +1227,7 @@ class BlackTestCase(unittest.TestCase): with patch("black.out", out), patch("black.err", err): with self.assertRaises(AssertionError): - self.assertFormatEqual("l = [1, 2, 3]", "l = [1, 2, 3,]") + self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]") out_str = "".join(out_lines) self.assertTrue("Expected tree:" in out_str) @@ -806,35 +1235,33 @@ class BlackTestCase(unittest.TestCase): self.assertEqual("".join(err_lines), "") def test_cache_broken_file(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir() as workspace: - cache_file = black.get_cache_file(black.DEFAULT_LINE_LENGTH, mode) + cache_file = black.get_cache_file(mode) with cache_file.open("w") as fobj: fobj.write("this is not a pickle") - self.assertEqual(black.read_cache(black.DEFAULT_LINE_LENGTH, mode), {}) + self.assertEqual(black.read_cache(mode), {}) src = (workspace / "test.py").resolve() with src.open("w") as fobj: fobj.write("print('hello')") - result = CliRunner().invoke(black.main, [str(src)]) - self.assertEqual(result.exit_code, 0) - cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode) + self.invokeBlack([str(src)]) + cache = black.read_cache(mode) self.assertIn(src, cache) def test_cache_single_file_already_cached(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir() as workspace: src = (workspace / "test.py").resolve() with src.open("w") as fobj: fobj.write("print('hello')") - black.write_cache({}, [src], black.DEFAULT_LINE_LENGTH, mode) - result = CliRunner().invoke(black.main, [str(src)]) - self.assertEqual(result.exit_code, 0) + black.write_cache({}, [src], mode) + self.invokeBlack([str(src)]) with src.open("r") as fobj: self.assertEqual(fobj.read(), "print('hello')") - @event_loop(close=False) + @event_loop() def test_cache_multiple_files(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir() as workspace, patch( "black.ProcessPoolExecutor", new=ThreadPoolExecutor ): @@ -844,48 +1271,48 @@ class BlackTestCase(unittest.TestCase): two = (workspace / "two.py").resolve() with two.open("w") as fobj: fobj.write("print('hello')") - black.write_cache({}, [one], black.DEFAULT_LINE_LENGTH, mode) - result = CliRunner().invoke(black.main, [str(workspace)]) - self.assertEqual(result.exit_code, 0) + black.write_cache({}, [one], mode) + self.invokeBlack([str(workspace)]) with one.open("r") as fobj: self.assertEqual(fobj.read(), "print('hello')") with two.open("r") as fobj: self.assertEqual(fobj.read(), 'print("hello")\n') - cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode) + cache = black.read_cache(mode) self.assertIn(one, cache) self.assertIn(two, cache) def test_no_cache_when_writeback_diff(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir() as workspace: src = (workspace / "test.py").resolve() with src.open("w") as fobj: fobj.write("print('hello')") - result = CliRunner().invoke(black.main, [str(src), "--diff"]) - self.assertEqual(result.exit_code, 0) - cache_file = black.get_cache_file(black.DEFAULT_LINE_LENGTH, mode) + self.invokeBlack([str(src), "--diff"]) + cache_file = black.get_cache_file(mode) self.assertFalse(cache_file.exists()) def test_no_cache_when_stdin(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir(): - result = CliRunner().invoke(black.main, ["-"], input="print('hello')") + result = CliRunner().invoke( + black.main, ["-"], input=BytesIO(b"print('hello')") + ) self.assertEqual(result.exit_code, 0) - cache_file = black.get_cache_file(black.DEFAULT_LINE_LENGTH, mode) + cache_file = black.get_cache_file(mode) self.assertFalse(cache_file.exists()) def test_read_cache_no_cachefile(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir(): - self.assertEqual(black.read_cache(black.DEFAULT_LINE_LENGTH, mode), {}) + self.assertEqual(black.read_cache(mode), {}) def test_write_cache_read_cache(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir() as workspace: src = (workspace / "test.py").resolve() src.touch() - black.write_cache({}, [src], black.DEFAULT_LINE_LENGTH, mode) - cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode) + black.write_cache({}, [src], mode) + cache = black.read_cache(mode) self.assertIn(src, cache) self.assertEqual(cache[src], black.get_cache_info(src)) @@ -906,15 +1333,15 @@ class BlackTestCase(unittest.TestCase): self.assertEqual(done, {cached}) def test_write_cache_creates_directory_if_needed(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir(exists=False) as workspace: self.assertFalse(workspace.exists()) - black.write_cache({}, [], black.DEFAULT_LINE_LENGTH, mode) + black.write_cache({}, [], mode) self.assertTrue(workspace.exists()) - @event_loop(close=False) + @event_loop() def test_failed_formatting_does_not_get_cached(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir() as workspace, patch( "black.ProcessPoolExecutor", new=ThreadPoolExecutor ): @@ -924,42 +1351,44 @@ class BlackTestCase(unittest.TestCase): clean = (workspace / "clean.py").resolve() with clean.open("w") as fobj: fobj.write('print("hello")\n') - result = CliRunner().invoke(black.main, [str(workspace)]) - self.assertEqual(result.exit_code, 123) - cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode) + self.invokeBlack([str(workspace)], exit_code=123) + cache = black.read_cache(mode) self.assertNotIn(failing, cache) self.assertIn(clean, cache) def test_write_cache_write_fail(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() with cache_dir(), patch.object(Path, "open") as mock: mock.side_effect = OSError - black.write_cache({}, [], black.DEFAULT_LINE_LENGTH, mode) + black.write_cache({}, [], mode) - @event_loop(close=False) + @event_loop() + @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError)) + def test_works_in_mono_process_only_environment(self) -> None: + with cache_dir() as workspace: + for f in [ + (workspace / "one.py").resolve(), + (workspace / "two.py").resolve(), + ]: + f.write_text('print("hello")\n') + self.invokeBlack([str(workspace)]) + + @event_loop() def test_check_diff_use_together(self) -> None: with cache_dir(): # Files which will be reformatted. src1 = (THIS_DIR / "data" / "string_quotes.py").resolve() - result = CliRunner().invoke(black.main, [str(src1), "--diff", "--check"]) - self.assertEqual(result.exit_code, 1, result.output) - + self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1) # Files which will not be reformatted. src2 = (THIS_DIR / "data" / "composition.py").resolve() - result = CliRunner().invoke(black.main, [str(src2), "--diff", "--check"]) - self.assertEqual(result.exit_code, 0, result.output) - + self.invokeBlack([str(src2), "--diff", "--check"]) # Multi file command. - result = CliRunner().invoke( - black.main, [str(src1), str(src2), "--diff", "--check"] - ) - self.assertEqual(result.exit_code, 1, result.output) + self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1) def test_no_files(self) -> None: with cache_dir(): # Without an argument, black exits with error code 0. - result = CliRunner().invoke(black.main, []) - self.assertEqual(result.exit_code, 0) + self.invokeBlack([]) def test_broken_symlink(self) -> None: with cache_dir() as workspace: @@ -968,43 +1397,49 @@ class BlackTestCase(unittest.TestCase): symlink.symlink_to("nonexistent.py") except OSError as e: self.skipTest(f"Can't create symlinks: {e}") - result = CliRunner().invoke(black.main, [str(workspace.resolve())]) - self.assertEqual(result.exit_code, 0) + self.invokeBlack([str(workspace.resolve())]) def test_read_cache_line_lengths(self) -> None: - mode = black.FileMode.AUTO_DETECT + mode = black.FileMode() + short_mode = black.FileMode(line_length=1) with cache_dir() as workspace: path = (workspace / "file.py").resolve() path.touch() - black.write_cache({}, [path], 1, mode) - one = black.read_cache(1, mode) + black.write_cache({}, [path], mode) + one = black.read_cache(mode) self.assertIn(path, one) - two = black.read_cache(2, mode) + two = black.read_cache(short_mode) self.assertNotIn(path, two) + def test_tricky_unicode_symbols(self) -> None: + source, expected = read_data("tricky_unicode_symbols") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + def test_single_file_force_pyi(self) -> None: - reg_mode = black.FileMode.AUTO_DETECT - pyi_mode = black.FileMode.PYI + reg_mode = black.FileMode() + pyi_mode = black.FileMode(is_pyi=True) contents, expected = read_data("force_pyi") with cache_dir() as workspace: path = (workspace / "file.py").resolve() with open(path, "w") as fh: fh.write(contents) - result = CliRunner().invoke(black.main, [str(path), "--pyi"]) - self.assertEqual(result.exit_code, 0) + self.invokeBlack([str(path), "--pyi"]) with open(path, "r") as fh: actual = fh.read() # verify cache with --pyi is separate - pyi_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, pyi_mode) + pyi_cache = black.read_cache(pyi_mode) self.assertIn(path, pyi_cache) - normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode) + normal_cache = black.read_cache(reg_mode) self.assertNotIn(path, normal_cache) self.assertEqual(actual, expected) - @event_loop(close=False) + @event_loop() def test_multi_file_force_pyi(self) -> None: - reg_mode = black.FileMode.AUTO_DETECT - pyi_mode = black.FileMode.PYI + reg_mode = black.FileMode() + pyi_mode = black.FileMode(is_pyi=True) contents, expected = read_data("force_pyi") with cache_dir() as workspace: paths = [ @@ -1014,49 +1449,49 @@ class BlackTestCase(unittest.TestCase): for path in paths: with open(path, "w") as fh: fh.write(contents) - result = CliRunner().invoke(black.main, [str(p) for p in paths] + ["--pyi"]) - self.assertEqual(result.exit_code, 0) + self.invokeBlack([str(p) for p in paths] + ["--pyi"]) for path in paths: with open(path, "r") as fh: actual = fh.read() self.assertEqual(actual, expected) # verify cache with --pyi is separate - pyi_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, pyi_mode) - normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode) + pyi_cache = black.read_cache(pyi_mode) + normal_cache = black.read_cache(reg_mode) for path in paths: self.assertIn(path, pyi_cache) self.assertNotIn(path, normal_cache) def test_pipe_force_pyi(self) -> None: source, expected = read_data("force_pyi") - result = CliRunner().invoke(black.main, ["-", "-q", "--pyi"], input=source) + result = CliRunner().invoke( + black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8")) + ) self.assertEqual(result.exit_code, 0) actual = result.output self.assertFormatEqual(actual, expected) def test_single_file_force_py36(self) -> None: - reg_mode = black.FileMode.AUTO_DETECT - py36_mode = black.FileMode.PYTHON36 + reg_mode = black.FileMode() + py36_mode = black.FileMode(target_versions=black.PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: path = (workspace / "file.py").resolve() with open(path, "w") as fh: fh.write(source) - result = CliRunner().invoke(black.main, [str(path), "--py36"]) - self.assertEqual(result.exit_code, 0) + self.invokeBlack([str(path), *PY36_ARGS]) with open(path, "r") as fh: actual = fh.read() - # verify cache with --py36 is separate - py36_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, py36_mode) + # verify cache with --target-version is separate + py36_cache = black.read_cache(py36_mode) self.assertIn(path, py36_cache) - normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode) + normal_cache = black.read_cache(reg_mode) self.assertNotIn(path, normal_cache) self.assertEqual(actual, expected) - @event_loop(close=False) + @event_loop() def test_multi_file_force_py36(self) -> None: - reg_mode = black.FileMode.AUTO_DETECT - py36_mode = black.FileMode.PYTHON36 + reg_mode = black.FileMode() + py36_mode = black.FileMode(target_versions=black.PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: paths = [ @@ -1066,24 +1501,32 @@ class BlackTestCase(unittest.TestCase): for path in paths: with open(path, "w") as fh: fh.write(source) - result = CliRunner().invoke( - black.main, [str(p) for p in paths] + ["--py36"] - ) - self.assertEqual(result.exit_code, 0) + self.invokeBlack([str(p) for p in paths] + PY36_ARGS) for path in paths: with open(path, "r") as fh: actual = fh.read() self.assertEqual(actual, expected) - # verify cache with --py36 is separate - pyi_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, py36_mode) - normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode) + # verify cache with --target-version is separate + pyi_cache = black.read_cache(py36_mode) + normal_cache = black.read_cache(reg_mode) for path in paths: self.assertIn(path, pyi_cache) self.assertNotIn(path, normal_cache) + def test_collections(self) -> None: + source, expected = read_data("collections") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + def test_pipe_force_py36(self) -> None: source, expected = read_data("force_py36") - result = CliRunner().invoke(black.main, ["-", "-q", "--py36"], input=source) + result = CliRunner().invoke( + black.main, + ["-", "-q", "--target-version=py36"], + input=BytesIO(source.encode("utf8")), + ) self.assertEqual(result.exit_code, 0) actual = result.output self.assertFormatEqual(actual, expected) @@ -1093,6 +1536,7 @@ class BlackTestCase(unittest.TestCase): include = re.compile(r"\.pyi?$") exclude = re.compile(r"/exclude/|/\.definitely_exclude/") report = black.Report() + gitignore = PathSpec.from_lines("gitwildmatch", []) sources: List[Path] = [] expected = [ Path(path / "b/dont_exclude/a.py"), @@ -1100,13 +1544,37 @@ class BlackTestCase(unittest.TestCase): ] this_abs = THIS_DIR.resolve() sources.extend( - black.gen_python_files_in_dir(path, this_abs, include, exclude, report) + black.gen_python_files( + path.iterdir(), this_abs, include, [exclude], report, gitignore + ) + ) + self.assertEqual(sorted(expected), sorted(sources)) + + def test_gitignore_exclude(self) -> None: + path = THIS_DIR / "data" / "include_exclude_tests" + include = re.compile(r"\.pyi?$") + exclude = re.compile(r"") + report = black.Report() + gitignore = PathSpec.from_lines( + "gitwildmatch", ["exclude/", ".definitely_exclude"] + ) + sources: List[Path] = [] + expected = [ + Path(path / "b/dont_exclude/a.py"), + Path(path / "b/dont_exclude/a.pyi"), + ] + this_abs = THIS_DIR.resolve() + sources.extend( + black.gen_python_files( + path.iterdir(), this_abs, include, [exclude], report, gitignore + ) ) self.assertEqual(sorted(expected), sorted(sources)) def test_empty_include(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" report = black.Report() + gitignore = PathSpec.from_lines("gitwildmatch", []) empty = re.compile(r"") sources: List[Path] = [] expected = [ @@ -1122,8 +1590,13 @@ class BlackTestCase(unittest.TestCase): ] this_abs = THIS_DIR.resolve() sources.extend( - black.gen_python_files_in_dir( - path, this_abs, empty, re.compile(black.DEFAULT_EXCLUDES), report + black.gen_python_files( + path.iterdir(), + this_abs, + empty, + [re.compile(black.DEFAULT_EXCLUDES)], + report, + gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1131,6 +1604,7 @@ class BlackTestCase(unittest.TestCase): def test_empty_exclude(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" report = black.Report() + gitignore = PathSpec.from_lines("gitwildmatch", []) empty = re.compile(r"") sources: List[Path] = [] expected = [ @@ -1143,16 +1617,20 @@ class BlackTestCase(unittest.TestCase): ] this_abs = THIS_DIR.resolve() sources.extend( - black.gen_python_files_in_dir( - path, this_abs, re.compile(black.DEFAULT_INCLUDES), empty, report + black.gen_python_files( + path.iterdir(), + this_abs, + re.compile(black.DEFAULT_INCLUDES), + [empty], + report, + gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) def test_invalid_include_exclude(self) -> None: for option in ["--include", "--exclude"]: - result = CliRunner().invoke(black.main, ["-", option, "**()(!!*)"]) - self.assertEqual(result.exit_code, 2) + self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) def test_preserves_line_endings(self) -> None: with TemporaryDirectory() as workspace: @@ -1162,50 +1640,61 @@ class BlackTestCase(unittest.TestCase): test_file.write_bytes(contents.encode()) ff(test_file, write_back=black.WriteBack.YES) updated_contents: bytes = test_file.read_bytes() - self.assertIn(nl.encode(), updated_contents) # type: ignore + self.assertIn(nl.encode(), updated_contents) if nl == "\n": - self.assertNotIn(b"\r\n", updated_contents) # type: ignore + self.assertNotIn(b"\r\n", updated_contents) + + def test_preserves_line_endings_via_stdin(self) -> None: + for nl in ["\n", "\r\n"]: + contents = nl.join(["def f( ):", " pass"]) + runner = BlackRunner() + result = runner.invoke( + black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8")) + ) + self.assertEqual(result.exit_code, 0) + output = runner.stdout_bytes + self.assertIn(nl.encode("utf8"), output) + if nl == "\n": + self.assertNotIn(b"\r\n", output) def test_assert_equivalent_different_asts(self) -> None: with self.assertRaises(AssertionError): black.assert_equivalent("{}", "None") def test_symlink_out_of_root_directory(self) -> None: - # prepare argumens path = MagicMock() root = THIS_DIR child = MagicMock() include = re.compile(black.DEFAULT_INCLUDES) exclude = re.compile(black.DEFAULT_EXCLUDES) report = black.Report() - - # set the behavior of mock arguments - # child should behave like a symlink which resolved path is clearly - # outside of the root directory + gitignore = PathSpec.from_lines("gitwildmatch", []) + # `child` should behave like a symlink which resolved path is clearly + # outside of the `root` directory. path.iterdir.return_value = [child] child.resolve.return_value = Path("/a/b/c") + child.as_posix.return_value = "/a/b/c" child.is_symlink.return_value = True - - # call the method - # it should not raise any error - list(black.gen_python_files_in_dir(path, root, include, exclude, report)) - - # check the call of the methods of the mock objects + try: + list( + black.gen_python_files( + path.iterdir(), root, include, exclude, report, gitignore + ) + ) + except ValueError as ve: + self.fail(f"`get_python_files_in_dir()` failed: {ve}") path.iterdir.assert_called_once() child.resolve.assert_called_once() child.is_symlink.assert_called_once() - - # set the behavior of mock arguments - # child should behave like a strange file which resolved path is clearly - # outside of the root directory + # `child` should behave like a strange file which resolved path is clearly + # outside of the `root` directory. child.is_symlink.return_value = False - - # call the method - # it should raise a ValueError with self.assertRaises(ValueError): - list(black.gen_python_files_in_dir(path, root, include, exclude, report)) - - # check the call of the methods of the mock objects + list( + black.gen_python_files( + path.iterdir(), root, include, exclude, report, gitignore + ) + ) path.iterdir.assert_called() self.assertEqual(path.iterdir.call_count, 2) child.resolve.assert_called() @@ -1235,6 +1724,207 @@ class BlackTestCase(unittest.TestCase): except RuntimeError as re: self.fail(f"`patch_click()` failed, exception still raised: {re}") + def test_root_logger_not_used_directly(self) -> None: + def fail(*args: Any, **kwargs: Any) -> None: + self.fail("Record created with root logger") + + with patch.multiple( + logging.root, + debug=fail, + info=fail, + warning=fail, + error=fail, + critical=fail, + log=fail, + ): + ff(THIS_FILE) + + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + def test_blackd_main(self) -> None: + with patch("blackd.web.run_app"): + result = CliRunner().invoke(blackd.main, []) + if result.exception is not None: + raise result.exception + self.assertEqual(result.exit_code, 0) + + def test_invalid_config_return_code(self) -> None: + tmp_file = Path(black.dump_to_file()) + try: + tmp_config = Path(black.dump_to_file()) + tmp_config.unlink() + args = ["--config", str(tmp_config), str(tmp_file)] + self.invokeBlack(args, exit_code=2, ignore_config=False) + finally: + tmp_file.unlink() + + +class BlackDTestCase(AioHTTPTestCase): + async def get_application(self) -> web.Application: + return blackd.make_app() + + # TODO: remove these decorators once the below is released + # https://github.com/aio-libs/aiohttp/pull/3727 + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_request_needs_formatting(self) -> None: + response = await self.client.post("/", data=b"print('hello world')") + self.assertEqual(response.status, 200) + self.assertEqual(response.charset, "utf8") + self.assertEqual(await response.read(), b'print("hello world")\n') + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_request_no_change(self) -> None: + response = await self.client.post("/", data=b'print("hello world")\n') + self.assertEqual(response.status, 204) + self.assertEqual(await response.read(), b"") + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_request_syntax_error(self) -> None: + response = await self.client.post("/", data=b"what even ( is") + self.assertEqual(response.status, 400) + content = await response.text() + self.assertTrue( + content.startswith("Cannot parse"), + msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", + ) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_unsupported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"} + ) + self.assertEqual(response.status, 501) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_supported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"} + ) + self.assertEqual(response.status, 200) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_invalid_python_variant(self) -> None: + async def check(header_value: str, expected_status: int = 400) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PYTHON_VARIANT_HEADER: header_value} + ) + self.assertEqual(response.status, expected_status) + + await check("lol") + await check("ruby3.5") + await check("pyi3.6") + await check("py1.5") + await check("2.8") + await check("py2.8") + await check("3.0") + await check("pypy3.0") + await check("jython3.4") + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_pyi(self) -> None: + source, expected = read_data("stub.pyi") + response = await self.client.post( + "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} + ) + self.assertEqual(response.status, 200) + self.assertEqual(await response.text(), expected) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_diff(self) -> None: + diff_header = re.compile( + rf"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) + + source, _ = read_data("blackd_diff.py") + expected, _ = read_data("blackd_diff.diff") + + response = await self.client.post( + "/", data=source, headers={blackd.DIFF_HEADER: "true"} + ) + self.assertEqual(response.status, 200) + + actual = await response.text() + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + self.assertEqual(actual, expected) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_python_variant(self) -> None: + code = ( + "def f(\n" + " and_has_a_bunch_of,\n" + " very_long_arguments_too,\n" + " and_lots_of_them_as_well_lol,\n" + " **and_very_long_keyword_arguments\n" + "):\n" + " pass\n" + ) + + async def check(header_value: str, expected_status: int) -> None: + response = await self.client.post( + "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} + ) + self.assertEqual( + response.status, expected_status, msg=await response.text() + ) + + await check("3.6", 200) + await check("py3.6", 200) + await check("3.6,3.7", 200) + await check("3.6,py3.7", 200) + await check("py36,py37", 200) + await check("36", 200) + await check("3.6.4", 200) + + await check("2", 204) + await check("2.7", 204) + await check("py2.7", 204) + await check("3.4", 204) + await check("py3.4", 204) + await check("py34,py36", 204) + await check("34", 204) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_line_length(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} + ) + self.assertEqual(response.status, 200) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_invalid_line_length(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "NaN"} + ) + self.assertEqual(response.status, 400) + + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_response_black_version_header(self) -> None: + response = await self.client.post("/") + self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) + if __name__ == "__main__": unittest.main(module="test_black")