X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/2893c42176903c8b6c28c46ff9e046861328b6a8..d4a85643a465f5fae2113d07d22d021d4af4795a:/tests/test_black.py diff --git a/tests/test_black.py b/tests/test_black.py index a633e67..dda1055 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -6,6 +6,7 @@ import io import logging import multiprocessing import os +import re import sys import types import unittest @@ -24,6 +25,7 @@ from typing import ( List, Optional, Sequence, + Type, TypeVar, Union, ) @@ -31,7 +33,6 @@ from unittest.mock import MagicMock, patch import click import pytest -import re from click import unstyle from click.testing import CliRunner from pathspec import PathSpec @@ -59,8 +60,8 @@ from tests.util import ( dump_to_stderr, ff, fs, - read_data, get_case_path, + read_data, read_data_from_file, ) @@ -153,6 +154,34 @@ class BlackTestCase(BlackBaseTestCase): os.unlink(tmp_file) self.assertFormatEqual(expected, actual) + @patch("black.dump_to_file", dump_to_stderr) + def test_one_empty_line(self) -> None: + mode = black.Mode(preview=True) + for nl in ["\n", "\r\n"]: + source = expected = nl + assert_format(source, expected, mode=mode) + + def test_one_empty_line_ff(self) -> None: + mode = black.Mode(preview=True) + for nl in ["\n", "\r\n"]: + expected = nl + tmp_file = Path(black.dump_to_file(nl)) + if system() == "Windows": + # Writing files in text mode automatically uses the system newline, + # but in this case we don't want this for testing reasons. See: + # https://github.com/psf/black/pull/3348 + with open(tmp_file, "wb") as f: + f.write(nl.encode("utf-8")) + try: + self.assertFalse( + ff(tmp_file, mode=mode, write_back=black.WriteBack.YES) + ) + with open(tmp_file, "rb") as f: + actual = f.read().decode("utf8") + finally: + os.unlink(tmp_file) + self.assertFormatEqual(expected, actual) + def test_experimental_string_processing_warns(self) -> None: self.assertWarns( black.mode.Deprecated, black.Mode, experimental_string_processing=True @@ -310,6 +339,26 @@ class BlackTestCase(BlackBaseTestCase): versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY38, versions) + def test_detect_debug_f_strings(self) -> None: + root = black.lib2to3_parse("""f"{x=}" """) + features = black.get_features_used(root) + self.assertIn(black.Feature.DEBUG_F_STRINGS, features) + versions = black.detect_target_versions(root) + self.assertIn(black.TargetVersion.PY38, versions) + + root = black.lib2to3_parse( + """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n""" + ) + features = black.get_features_used(root) + self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features) + + # We don't yet support feature version detection in nested f-strings + root = black.lib2to3_parse( + """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """ + ) + features = black.get_features_used(root) + self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features) + @patch("black.dump_to_file", dump_to_stderr) def test_string_quotes(self) -> None: source, expected = read_data("miscellaneous", "string_quotes") @@ -321,6 +370,30 @@ class BlackTestCase(BlackBaseTestCase): black.assert_equivalent(source, not_normalized) black.assert_stable(source, not_normalized, mode=mode) + def test_skip_source_first_line(self) -> None: + source, _ = read_data("miscellaneous", "invalid_header") + tmp_file = Path(black.dump_to_file(source)) + # Full source should fail (invalid syntax at header) + self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123) + # So, skipping the first line should work + result = BlackRunner().invoke( + black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"] + ) + self.assertEqual(result.exit_code, 0) + with open(tmp_file, encoding="utf8") as f: + actual = f.read() + self.assertFormatEqual(source, actual) + + def test_skip_source_first_line_when_mixing_newlines(self) -> None: + code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n" + expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n" + with TemporaryDirectory() as workspace: + test_file = Path(workspace) / "skip_header.py" + test_file.write_bytes(code_mixing_newlines) + mode = replace(DEFAULT_MODE, skip_source_first_line=True) + ff(test_file, mode=mode, write_back=black.WriteBack.YES) + self.assertEqual(test_file.read_bytes(), expected) + def test_skip_magic_trailing_comma(self) -> None: source, _ = read_data("simple_cases", "expression") expected, _ = read_data( @@ -446,8 +519,10 @@ class BlackTestCase(BlackBaseTestCase): 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) @@ -456,8 +531,10 @@ class BlackTestCase(BlackBaseTestCase): 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") @@ -466,8 +543,10 @@ class BlackTestCase(BlackBaseTestCase): 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") @@ -476,8 +555,10 @@ class BlackTestCase(BlackBaseTestCase): 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) @@ -486,22 +567,28 @@ class BlackTestCase(BlackBaseTestCase): 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.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) def test_report_quiet(self) -> None: @@ -543,8 +630,10 @@ class BlackTestCase(BlackBaseTestCase): 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) @@ -552,8 +641,10 @@ class BlackTestCase(BlackBaseTestCase): 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") @@ -562,8 +653,10 @@ class BlackTestCase(BlackBaseTestCase): 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") @@ -571,8 +664,10 @@ class BlackTestCase(BlackBaseTestCase): 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) @@ -580,22 +675,28 @@ class BlackTestCase(BlackBaseTestCase): 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.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) def test_report_normal(self) -> None: @@ -639,8 +740,10 @@ class BlackTestCase(BlackBaseTestCase): 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) @@ -649,8 +752,10 @@ class BlackTestCase(BlackBaseTestCase): 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") @@ -659,8 +764,10 @@ class BlackTestCase(BlackBaseTestCase): 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") @@ -668,8 +775,10 @@ class BlackTestCase(BlackBaseTestCase): 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) @@ -677,22 +786,28 @@ class BlackTestCase(BlackBaseTestCase): 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.", + ( + "2 files would be reformatted, 3 files would be left unchanged, 2" + " files would fail to reformat." + ), ) def test_lib2to3_parse(self) -> None: @@ -804,6 +919,12 @@ class BlackTestCase(BlackBaseTestCase): self.assertEqual(black.get_features_used(node), set()) node = black.lib2to3_parse("try: pass\nexcept *Group: pass") self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR}) + node = black.lib2to3_parse("a[*b]") + self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) + node = black.lib2to3_parse("a[x, *y(), z] = t") + self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) + node = black.lib2to3_parse("def fn(*args: *T): pass") + self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) def test_get_features_used_for_future_flags(self) -> None: for src, features in [ @@ -879,8 +1000,8 @@ class BlackTestCase(BlackBaseTestCase): ) def test_format_file_contents(self) -> None: - empty = "" mode = DEFAULT_MODE + empty = "" with self.assertRaises(black.NothingChanged): black.format_file_contents(empty, mode=mode, fast=False) just_nl = "\n" @@ -898,6 +1019,17 @@ class BlackTestCase(BlackBaseTestCase): black.format_file_contents(invalid, mode=mode, fast=False) self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can") + mode = black.Mode(preview=True) + just_crlf = "\r\n" + with self.assertRaises(black.NothingChanged): + black.format_file_contents(just_crlf, mode=mode, fast=False) + just_whitespace_nl = "\n\t\n \n\t \n \t\n\n" + actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False) + self.assertEqual("\n", actual) + just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n" + actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False) + self.assertEqual("\r\n", actual) + def test_endmarker(self) -> None: n = black.lib2to3_parse("\n") self.assertEqual(n.type, black.syms.file_input) @@ -1189,8 +1321,51 @@ class BlackTestCase(BlackBaseTestCase): report.done.assert_called_with(expected, black.Changed.YES) def test_reformat_one_with_stdin_empty(self) -> None: + cases = [ + ("", ""), + ("\n", "\n"), + ("\r\n", "\r\n"), + (" \t", ""), + (" \t\n\t ", "\n"), + (" \t\r\n\t ", "\r\n"), + ] + + def _new_wrapper( + output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper] + ) -> Callable[[Any, Any], io.TextIOWrapper]: + def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper: + if args == (sys.stdout.buffer,): + # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`, + # return our mock object. + return output + # It's something else (i.e. `decode_bytes()`) calling + # `io.TextIOWrapper()`, pass through to the original implementation. + # See discussion in https://github.com/psf/black/pull/2489 + return io_TextIOWrapper(*args, **kwargs) + + return get_output + + mode = black.Mode(preview=True) + for content, expected in cases: + output = io.StringIO() + io_TextIOWrapper = io.TextIOWrapper + + with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)): + try: + black.format_stdin_to_stdout( + fast=True, + content=content, + write_back=black.WriteBack.YES, + mode=mode, + ) + except io.UnsupportedOperation: + pass # StringIO does not support detach + assert output.getvalue() == expected + + # An empty string is the only test case for `preview=False` output = io.StringIO() - with patch("io.TextIOWrapper", lambda *args, **kwargs: output): + io_TextIOWrapper = io.TextIOWrapper + with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)): try: black.format_stdin_to_stdout( fast=True, @@ -1260,6 +1435,17 @@ class BlackTestCase(BlackBaseTestCase): if nl == "\n": self.assertNotIn(b"\r\n", output) + def test_normalize_line_endings(self) -> None: + with TemporaryDirectory() as workspace: + test_file = Path(workspace) / "test.py" + for data, expected in ( + (b"c\r\nc\n ", b"c\r\nc\r\n"), + (b"l\nl\r\n ", b"l\nl\n"), + ): + test_file.write_bytes(data) + ff(test_file, write_back=black.WriteBack.YES) + self.assertEqual(test_file.read_bytes(), expected) + def test_assert_equivalent_different_asts(self) -> None: with self.assertRaises(AssertionError): black.assert_equivalent("{}", "None") @@ -1370,6 +1556,12 @@ class BlackTestCase(BlackBaseTestCase): (src_dir.resolve(), "pyproject.toml"), ) + with change_directory(test_dir): + self.assertEqual( + black.find_project_root(("-",), stdin_filename="../src/a.py"), + (src_dir.resolve(), "pyproject.toml"), + ) + @patch( "black.files.find_user_pyproject_toml", ) @@ -1455,7 +1647,6 @@ class BlackTestCase(BlackBaseTestCase): black.assert_stable(source, output, mode=DEFAULT_MODE) def test_bpo_2142_workaround(self) -> None: - # https://bugs.python.org/issue2142 source, _ = read_data("miscellaneous", "missing_final_newline") @@ -1732,7 +1923,9 @@ class TestCaching: src = (workspace / f"test{tag}.py").resolve() with src.open("w") as fobj: fobj.write("print('hello')") - with patch("black.Manager", wraps=multiprocessing.Manager) as mgr: + with patch( + "black.concurrency.Manager", wraps=multiprocessing.Manager + ) as mgr: cmd = ["--diff", str(workspace)] if color: cmd.append("--color") @@ -1779,7 +1972,7 @@ class TestCaching: str(cached): black.get_cache_info(cached), str(cached_but_changed): (0.0, 0), } - todo, done = black.filter_cached( + todo, done = black.cache.filter_cached( cache, {uncached, cached, cached_but_changed} ) assert todo == {uncached, cached_but_changed} @@ -1888,6 +2081,17 @@ class TestFileCollection: ctx.obj["root"] = base assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/") + def test_gitignore_used_on_multiple_sources(self) -> None: + root = Path(DATA_DIR / "gitignore_used_on_multiple_sources") + expected = [ + root / "dir1" / "b.py", + root / "dir2" / "b.py", + ] + ctx = FakeContext() + ctx.obj["root"] = root + src = [root / "dir1", root / "dir2"] + assert_collected_sources(src, expected, ctx=ctx) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_exclude_for_issue_1572(self) -> None: # Exclude shouldn't touch files that were explicitly given to Black through the @@ -1921,7 +2125,7 @@ class TestFileCollection: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, ) @@ -1950,13 +2154,20 @@ class TestFileCollection: None, None, report, - root_gitignore, + {path: root_gitignore}, verbose=False, quiet=False, ) ) assert sorted(expected) == sorted(sources) + def test_nested_gitignore_directly_in_source_directory(self) -> None: + # https://github.com/psf/black/issues/2598 + path = Path(DATA_DIR / "nested_gitignore_tests") + src = Path(path / "root" / "child") + expected = [src / "a.py", src / "c.py"] + assert_collected_sources([src], expected) + def test_invalid_gitignore(self) -> None: path = THIS_DIR / "data" / "invalid_gitignore_tests" empty_config = path / "pyproject.toml" @@ -1981,6 +2192,32 @@ class TestFileCollection: gitignore = path / "a" / ".gitignore" assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + def test_gitignore_that_ignores_subfolders(self) -> None: + # If gitignore with */* is in root + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir") + expected = [root / "b.py"] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([root], expected, ctx=ctx) + + # If .gitignore with */* is nested + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") + expected = [ + root / "a.py", + root / "subdir" / "b.py", + ] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([root], expected, ctx=ctx) + + # If command is executed from outer dir + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") + target = root / "subdir" + expected = [target / "b.py"] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([target], expected, ctx=ctx) + def test_empty_include(self) -> None: path = DATA_DIR / "include_exclude_tests" src = [path] @@ -2035,7 +2272,7 @@ class TestFileCollection: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, )