X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/dea81b7ad5cfa04c3572771c34af823449d0a8f3..16275d24e3c2ad88d953d67e7d8795fd7f73f7cb:/tests/test_black.py?ds=inline diff --git a/tests/test_black.py b/tests/test_black.py index a688c87..398a528 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -6,7 +6,8 @@ from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from dataclasses import replace import inspect -from io import BytesIO, TextIOWrapper +import io +from io import BytesIO import os from pathlib import Path from platform import system @@ -16,16 +17,16 @@ from tempfile import TemporaryDirectory import types from typing import ( Any, - BinaryIO, Callable, Dict, - Generator, List, Iterator, TypeVar, ) +import pytest import unittest from unittest.mock import patch, MagicMock +from parameterized import parameterized import click from click import unstyle @@ -33,12 +34,18 @@ from click.testing import CliRunner import black from black import Feature, TargetVersion +from black.cache import get_cache_file +from black.debug import DebugVisitor +from black.output import diff, color_diff +from black.report import Report +import black.files from pathspec import PathSpec # Import other test classes from tests.util import ( THIS_DIR, + change_directory, read_data, DETERMINISTIC_HEADER, BlackBaseTestCase, @@ -47,7 +54,6 @@ from tests.util import ( ff, dump_to_stderr, ) -from .test_primer import PrimerCLITests # noqa: F401 THIS_FILE = Path(__file__) @@ -61,6 +67,9 @@ PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERS T = TypeVar("T") R = TypeVar("R") +# Match the time output in a diff, but nothing else +DIFF_TIME = re.compile(r"\t[\d-:+\. ]+") + @contextmanager def cache_dir(exists: bool = True) -> Iterator[Path]: @@ -68,7 +77,7 @@ def cache_dir(exists: bool = True) -> Iterator[Path]: cache_dir = Path(workspace) if not exists: cache_dir = cache_dir / "new" - with patch("black.CACHE_DIR", cache_dir): + with patch("black.cache.CACHE_DIR", cache_dir): yield cache_dir @@ -99,28 +108,10 @@ class FakeParameter(click.Parameter): class BlackRunner(CliRunner): - """Modify CliRunner so that stderr is not merged with stdout. - - This is a hack that can be removed once we depend on Click 7.x""" + """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI.""" def __init__(self) -> None: - self.stderrbuf = BytesIO() - self.stdoutbuf = BytesIO() - self.stdout_bytes = b"" - self.stderr_bytes = b"" - super().__init__() - - @contextmanager - def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]: - with super().isolation(*args, **kwargs) as output: - try: - hold_stderr = sys.stderr - 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 + super().__init__(mix_stderr=False) class BlackTestCase(BlackBaseTestCase): @@ -131,13 +122,15 @@ class BlackTestCase(BlackBaseTestCase): if ignore_config: args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args] result = runner.invoke(black.main, args) + assert result.stdout_bytes is not None + assert result.stderr_bytes is not None 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"stdout: {result.stdout_bytes.decode()!r}\n" + f"stderr: {result.stderr_bytes.decode()!r}\n" f"exception: {result.exception}" ), ) @@ -170,8 +163,9 @@ class BlackTestCase(BlackBaseTestCase): ) self.assertEqual(result.exit_code, 0) self.assertFormatEqual(expected, result.output) - black.assert_equivalent(source, result.output) - black.assert_stable(source, result.output, DEFAULT_MODE) + if source != result.output: + black.assert_equivalent(source, result.output) + black.assert_stable(source, result.output, DEFAULT_MODE) def test_piping_diff(self) -> None: diff_header = re.compile( @@ -254,6 +248,24 @@ class BlackTestCase(BlackBaseTestCase): actual = fs(source) black.assert_stable(source, actual, DEFAULT_MODE) + @patch("black.dump_to_file", dump_to_stderr) + def test_trailing_comma_optional_parens_stability1_pass2(self) -> None: + source, _expected = read_data("trailing_comma_optional_parens1") + actual = fs(fs(source)) # this is what `format_file_contents` does with --safe + black.assert_stable(source, actual, DEFAULT_MODE) + + @patch("black.dump_to_file", dump_to_stderr) + def test_trailing_comma_optional_parens_stability2_pass2(self) -> None: + source, _expected = read_data("trailing_comma_optional_parens2") + actual = fs(fs(source)) # this is what `format_file_contents` does with --safe + black.assert_stable(source, actual, DEFAULT_MODE) + + @patch("black.dump_to_file", dump_to_stderr) + def test_trailing_comma_optional_parens_stability3_pass2(self) -> None: + source, _expected = read_data("trailing_comma_optional_parens3") + actual = fs(fs(source)) # this is what `format_file_contents` does with --safe + black.assert_stable(source, actual, DEFAULT_MODE) + @patch("black.dump_to_file", dump_to_stderr) def test_pep_572(self) -> None: source, expected = read_data("pep_572") @@ -263,6 +275,23 @@ class BlackTestCase(BlackBaseTestCase): if sys.version_info >= (3, 8): black.assert_equivalent(source, actual) + @patch("black.dump_to_file", dump_to_stderr) + def test_pep_572_remove_parens(self) -> None: + source, expected = read_data("pep_572_remove_parens") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_stable(source, actual, DEFAULT_MODE) + if sys.version_info >= (3, 8): + black.assert_equivalent(source, actual) + + @patch("black.dump_to_file", dump_to_stderr) + def test_pep_572_do_not_remove_parens(self) -> None: + source, expected = read_data("pep_572_do_not_remove_parens") + # the AST safety checks will fail, but that's expected, just make sure no + # parentheses are touched + actual = black.format_str(source, mode=DEFAULT_MODE) + self.assertFormatEqual(expected, actual) + def test_pep_572_version_detection(self) -> None: source, _ = read_data("pep_572") root = black.lib2to3_parse(source) @@ -271,6 +300,14 @@ class BlackTestCase(BlackBaseTestCase): versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY38, versions) + @parameterized.expand([(3, 9), (3, 10)]) + def test_pep_572_newer_syntax(self, major: int, minor: int) -> None: + source, expected = read_data(f"pep_572_py{major}{minor}") + actual = fs(source, mode=DEFAULT_MODE) + self.assertFormatEqual(expected, actual) + if sys.version_info >= (major, minor): + black.assert_equivalent(source, actual) + def test_expression_ff(self) -> None: source, expected = read_data("expression") tmp_file = Path(black.dump_to_file(source)) @@ -287,6 +324,7 @@ class BlackTestCase(BlackBaseTestCase): def test_expression_diff(self) -> None: source, _ = read_data("expression.py") + config = THIS_DIR / "data" / "empty_pyproject.toml" expected, _ = read_data("expression.diff") tmp_file = Path(black.dump_to_file(source)) diff_header = re.compile( @@ -294,13 +332,14 @@ class BlackTestCase(BlackBaseTestCase): r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" ) try: - result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)]) + result = BlackRunner().invoke( + black.main, ["--diff", str(tmp_file), f"--config={config}"] + ) self.assertEqual(result.exit_code, 0) finally: os.unlink(tmp_file) actual = result.output 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 = ( @@ -312,11 +351,12 @@ class BlackTestCase(BlackBaseTestCase): def test_expression_diff_with_color(self) -> None: source, _ = read_data("expression.py") + config = THIS_DIR / "data" / "empty_pyproject.toml" expected, _ = read_data("expression.diff") tmp_file = Path(black.dump_to_file(source)) try: result = BlackRunner().invoke( - black.main, ["--diff", "--color", str(tmp_file)] + black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"] ) finally: os.unlink(tmp_file) @@ -349,11 +389,12 @@ class BlackTestCase(BlackBaseTestCase): @patch("black.dump_to_file", dump_to_stderr) def test_string_quotes(self) -> None: source, expected = read_data("string_quotes") - actual = fs(source) + mode = black.Mode(experimental_string_processing=True) + actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, DEFAULT_MODE) - mode = replace(DEFAULT_MODE, string_normalization=False) + black.assert_stable(source, actual, mode) + mode = replace(mode, string_normalization=False) not_normalized = fs(source, mode=mode) self.assertFormatEqual(source.replace("\\\n", ""), not_normalized) black.assert_equivalent(source, not_normalized) @@ -395,6 +436,32 @@ class BlackTestCase(BlackBaseTestCase): black.assert_equivalent(source, actual) black.assert_stable(source, actual, mode) + def test_skip_magic_trailing_comma(self) -> None: + source, _ = read_data("expression.py") + expected, _ = read_data("expression_skip_magic_trailing_comma.diff") + 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 " + r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) + try: + result = BlackRunner().invoke(black.main, ["-C", "--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 = actual.rstrip() + "\n" # the diff output has a trailing space + if expected != actual: + dump = black.dump_to_file(actual) + msg = ( + "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_skip_magic_trailing_comma.diff with {dump}" + ) + self.assertEqual(expected, actual, msg) + + @pytest.mark.python2 @patch("black.dump_to_file", dump_to_stderr) def test_python2_print_function(self) -> None: source, expected = read_data("python2_print_function") @@ -485,7 +552,7 @@ class BlackTestCase(BlackBaseTestCase): self.assertFormatEqual(contents_spc, fs(contents_tab)) def test_report_verbose(self) -> None: - report = black.Report(verbose=True) + report = Report(verbose=True) out_lines = [] err_lines = [] @@ -495,7 +562,7 @@ class BlackTestCase(BlackBaseTestCase): def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): report.done(Path("f1"), black.Changed.NO) self.assertEqual(len(out_lines), 1) self.assertEqual(len(err_lines), 0) @@ -587,7 +654,7 @@ class BlackTestCase(BlackBaseTestCase): ) def test_report_quiet(self) -> None: - report = black.Report(quiet=True) + report = Report(quiet=True) out_lines = [] err_lines = [] @@ -597,7 +664,7 @@ class BlackTestCase(BlackBaseTestCase): def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): report.done(Path("f1"), black.Changed.NO) self.assertEqual(len(out_lines), 0) self.assertEqual(len(err_lines), 0) @@ -691,7 +758,7 @@ class BlackTestCase(BlackBaseTestCase): def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): report.done(Path("f1"), black.Changed.NO) self.assertEqual(len(out_lines), 0) self.assertEqual(len(err_lines), 0) @@ -908,8 +975,8 @@ class BlackTestCase(BlackBaseTestCase): def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): - black.DebugVisitor.show(source) + with patch("black.debug.out", out): + DebugVisitor.show(source) actual = "\n".join(out_lines) + "\n" log_name = "" if expected != actual: @@ -957,7 +1024,7 @@ class BlackTestCase(BlackBaseTestCase): def err(msg: str, **kwargs: Any) -> None: err_lines.append(msg) - with patch("black.out", out), patch("black.err", err): + with patch("black.output._out", out), patch("black.output._err", err): with self.assertRaises(AssertionError): self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]") @@ -969,7 +1036,7 @@ class BlackTestCase(BlackBaseTestCase): def test_cache_broken_file(self) -> None: mode = DEFAULT_MODE with cache_dir() as workspace: - cache_file = black.get_cache_file(mode) + cache_file = get_cache_file(mode) with cache_file.open("w") as fobj: fobj.write("this is not a pickle") self.assertEqual(black.read_cache(mode), {}) @@ -978,7 +1045,7 @@ class BlackTestCase(BlackBaseTestCase): fobj.write("print('hello')") self.invokeBlack([str(src)]) cache = black.read_cache(mode) - self.assertIn(src, cache) + self.assertIn(str(src), cache) def test_cache_single_file_already_cached(self) -> None: mode = DEFAULT_MODE @@ -1010,8 +1077,8 @@ class BlackTestCase(BlackBaseTestCase): with two.open("r") as fobj: self.assertEqual(fobj.read(), 'print("hello")\n') cache = black.read_cache(mode) - self.assertIn(one, cache) - self.assertIn(two, cache) + self.assertIn(str(one), cache) + self.assertIn(str(two), cache) def test_no_cache_when_writeback_diff(self) -> None: mode = DEFAULT_MODE @@ -1023,7 +1090,7 @@ class BlackTestCase(BlackBaseTestCase): "black.write_cache" ) as write_cache: self.invokeBlack([str(src), "--diff"]) - cache_file = black.get_cache_file(mode) + cache_file = get_cache_file(mode) self.assertFalse(cache_file.exists()) write_cache.assert_not_called() read_cache.assert_not_called() @@ -1038,7 +1105,7 @@ class BlackTestCase(BlackBaseTestCase): "black.write_cache" ) as write_cache: self.invokeBlack([str(src), "--diff", "--color"]) - cache_file = black.get_cache_file(mode) + cache_file = get_cache_file(mode) self.assertFalse(cache_file.exists()) write_cache.assert_not_called() read_cache.assert_not_called() @@ -1076,7 +1143,7 @@ class BlackTestCase(BlackBaseTestCase): black.main, ["-"], input=BytesIO(b"print('hello')") ) self.assertEqual(result.exit_code, 0) - cache_file = black.get_cache_file(mode) + cache_file = get_cache_file(mode) self.assertFalse(cache_file.exists()) def test_read_cache_no_cachefile(self) -> None: @@ -1091,8 +1158,8 @@ class BlackTestCase(BlackBaseTestCase): src.touch() black.write_cache({}, [src], mode) cache = black.read_cache(mode) - self.assertIn(src, cache) - self.assertEqual(cache[src], black.get_cache_info(src)) + self.assertIn(str(src), cache) + self.assertEqual(cache[str(src)], black.get_cache_info(src)) def test_filter_cached(self) -> None: with TemporaryDirectory() as workspace: @@ -1103,7 +1170,10 @@ class BlackTestCase(BlackBaseTestCase): uncached.touch() cached.touch() cached_but_changed.touch() - cache = {cached: black.get_cache_info(cached), cached_but_changed: (0.0, 0)} + cache = { + str(cached): black.get_cache_info(cached), + str(cached_but_changed): (0.0, 0), + } todo, done = black.filter_cached( cache, {uncached, cached, cached_but_changed} ) @@ -1131,8 +1201,8 @@ class BlackTestCase(BlackBaseTestCase): fobj.write('print("hello")\n') self.invokeBlack([str(workspace)], exit_code=123) cache = black.read_cache(mode) - self.assertNotIn(failing, cache) - self.assertIn(clean, cache) + self.assertNotIn(str(failing), cache) + self.assertIn(str(clean), cache) def test_write_cache_write_fail(self) -> None: mode = DEFAULT_MODE @@ -1185,9 +1255,9 @@ class BlackTestCase(BlackBaseTestCase): path.touch() black.write_cache({}, [path], mode) one = black.read_cache(mode) - self.assertIn(path, one) + self.assertIn(str(path), one) two = black.read_cache(short_mode) - self.assertNotIn(path, two) + self.assertNotIn(str(path), two) def test_single_file_force_pyi(self) -> None: pyi_mode = replace(DEFAULT_MODE, is_pyi=True) @@ -1201,9 +1271,9 @@ class BlackTestCase(BlackBaseTestCase): actual = fh.read() # verify cache with --pyi is separate pyi_cache = black.read_cache(pyi_mode) - self.assertIn(path, pyi_cache) + self.assertIn(str(path), pyi_cache) normal_cache = black.read_cache(DEFAULT_MODE) - self.assertNotIn(path, normal_cache) + self.assertNotIn(str(path), normal_cache) self.assertFormatEqual(expected, actual) black.assert_equivalent(contents, actual) black.assert_stable(contents, actual, pyi_mode) @@ -1230,8 +1300,8 @@ class BlackTestCase(BlackBaseTestCase): 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) + self.assertIn(str(path), pyi_cache) + self.assertNotIn(str(path), normal_cache) def test_pipe_force_pyi(self) -> None: source, expected = read_data("force_pyi") @@ -1255,9 +1325,9 @@ class BlackTestCase(BlackBaseTestCase): actual = fh.read() # verify cache with --target-version is separate py36_cache = black.read_cache(py36_mode) - self.assertIn(path, py36_cache) + self.assertIn(str(path), py36_cache) normal_cache = black.read_cache(reg_mode) - self.assertNotIn(path, normal_cache) + self.assertNotIn(str(path), normal_cache) self.assertEqual(actual, expected) @event_loop() @@ -1282,8 +1352,8 @@ class BlackTestCase(BlackBaseTestCase): 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) + self.assertIn(str(path), pyi_cache) + self.assertNotIn(str(path), normal_cache) def test_pipe_force_py36(self) -> None: source, expected = read_data("force_py36") @@ -1310,7 +1380,42 @@ class BlackTestCase(BlackBaseTestCase): this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, exclude, None, report, gitignore + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + gitignore, + verbose=False, + quiet=False, + ) + ) + self.assertEqual(sorted(expected), sorted(sources)) + + def test_gitignore_used_as_default(self) -> None: + path = Path(THIS_DIR / "data" / "include_exclude_tests") + include = re.compile(r"\.pyi?$") + extend_exclude = re.compile(r"/exclude/") + src = str(path / "b/") + report = black.Report() + expected: List[Path] = [ + path / "b/.definitely_exclude/a.py", + path / "b/.definitely_exclude/a.pyi", + ] + sources = list( + black.get_sources( + ctx=FakeContext(), + src=(src,), + quiet=True, + verbose=False, + include=include, + exclude=None, + extend_exclude=extend_exclude, + force_exclude=None, + report=report, + stdin_filename=None, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1332,8 +1437,9 @@ class BlackTestCase(BlackBaseTestCase): src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1354,8 +1460,9 @@ class BlackTestCase(BlackBaseTestCase): src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1377,8 +1484,9 @@ class BlackTestCase(BlackBaseTestCase): src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), + extend_exclude=None, force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1388,7 +1496,7 @@ class BlackTestCase(BlackBaseTestCase): @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) def test_get_sources_with_stdin_filename_and_exclude(self) -> None: - # Exclude shouldn't exclude stdin_filename since it is mimicing the + # Exclude shouldn't exclude stdin_filename since it is mimicking the # file being passed directly. This is the same as # test_exclude_for_issue_1572 path = THIS_DIR / "data" / "include_exclude_tests" @@ -1404,8 +1512,37 @@ class BlackTestCase(BlackBaseTestCase): src=(src,), quiet=True, verbose=False, - include=include, - exclude=exclude, + include=re.compile(include), + exclude=re.compile(exclude), + extend_exclude=None, + force_exclude=None, + report=report, + stdin_filename=stdin_filename, + ) + ) + self.assertEqual(sorted(expected), sorted(sources)) + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: + # Extend exclude shouldn't exclude stdin_filename since it is mimicking the + # file being passed directly. This is the same as + # test_exclude_for_issue_1572 + path = THIS_DIR / "data" / "include_exclude_tests" + include = "" + extend_exclude = r"/exclude/|a\.py" + src = "-" + report = black.Report() + stdin_filename = str(path / "b/exclude/a.py") + expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")] + sources = list( + black.get_sources( + ctx=FakeContext(), + src=(src,), + quiet=True, + verbose=False, + include=re.compile(include), + exclude=re.compile(""), + extend_exclude=re.compile(extend_exclude), force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1429,9 +1566,10 @@ class BlackTestCase(BlackBaseTestCase): src=(src,), quiet=True, verbose=False, - include=include, - exclude="", - force_exclude=force_exclude, + include=re.compile(include), + exclude=re.compile(""), + extend_exclude=None, + force_exclude=re.compile(force_exclude), report=report, stdin_filename=stdin_filename, ) @@ -1471,8 +1609,58 @@ class BlackTestCase(BlackBaseTestCase): mode=DEFAULT_MODE, report=report, ) - fsts.assert_called_once() - # __BLACK_STDIN_FILENAME__ should have been striped + fsts.assert_called_once_with( + fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE + ) + # __BLACK_STDIN_FILENAME__ should have been stripped + report.done.assert_called_with(expected, black.Changed.YES) + + def test_reformat_one_with_stdin_filename_pyi(self) -> None: + with patch( + "black.format_stdin_to_stdout", + return_value=lambda *args, **kwargs: black.Changed.YES, + ) as fsts: + report = MagicMock() + p = "foo.pyi" + path = Path(f"__BLACK_STDIN_FILENAME__{p}") + expected = Path(p) + black.reformat_one( + path, + fast=True, + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + report=report, + ) + fsts.assert_called_once_with( + fast=True, + write_back=black.WriteBack.YES, + mode=replace(DEFAULT_MODE, is_pyi=True), + ) + # __BLACK_STDIN_FILENAME__ should have been stripped + report.done.assert_called_with(expected, black.Changed.YES) + + def test_reformat_one_with_stdin_filename_ipynb(self) -> None: + with patch( + "black.format_stdin_to_stdout", + return_value=lambda *args, **kwargs: black.Changed.YES, + ) as fsts: + report = MagicMock() + p = "foo.ipynb" + path = Path(f"__BLACK_STDIN_FILENAME__{p}") + expected = Path(p) + black.reformat_one( + path, + fast=True, + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + report=report, + ) + fsts.assert_called_once_with( + fast=True, + write_back=black.WriteBack.YES, + mode=replace(DEFAULT_MODE, is_ipynb=True), + ) + # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) def test_reformat_one_with_stdin_and_existing_path(self) -> None: @@ -1496,9 +1684,23 @@ class BlackTestCase(BlackBaseTestCase): report=report, ) fsts.assert_called_once() - # __BLACK_STDIN_FILENAME__ should have been striped + # __BLACK_STDIN_FILENAME__ should have been stripped report.done.assert_called_with(expected, black.Changed.YES) + def test_reformat_one_with_stdin_empty(self) -> None: + output = io.StringIO() + with patch("io.TextIOWrapper", lambda *args, **kwargs: output): + try: + black.format_stdin_to_stdout( + fast=True, + content="", + write_back=black.WriteBack.YES, + mode=DEFAULT_MODE, + ) + except io.UnsupportedOperation: + pass # StringIO does not support detach + assert output.getvalue() == "" + def test_gitignore_exclude(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" include = re.compile(r"\.pyi?$") @@ -1515,11 +1717,73 @@ class BlackTestCase(BlackBaseTestCase): this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, exclude, None, report, gitignore + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + gitignore, + verbose=False, + quiet=False, + ) + ) + self.assertEqual(sorted(expected), sorted(sources)) + + def test_nested_gitignore(self) -> None: + path = Path(THIS_DIR / "data" / "nested_gitignore_tests") + include = re.compile(r"\.pyi?$") + exclude = re.compile(r"") + root_gitignore = black.files.get_gitignore(path) + report = black.Report() + expected: List[Path] = [ + Path(path / "x.py"), + Path(path / "root/b.py"), + Path(path / "root/c.py"), + Path(path / "root/child/c.py"), + ] + this_abs = THIS_DIR.resolve() + sources = list( + black.gen_python_files( + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + root_gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) + def test_invalid_gitignore(self) -> None: + path = THIS_DIR / "data" / "invalid_gitignore_tests" + empty_config = path / "pyproject.toml" + result = BlackRunner().invoke( + black.main, ["--verbose", "--config", str(empty_config), str(path)] + ) + assert result.exit_code == 1 + assert result.stderr_bytes is not None + + gitignore = path / ".gitignore" + assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + + def test_invalid_nested_gitignore(self) -> None: + path = THIS_DIR / "data" / "invalid_nested_gitignore_tests" + empty_config = path / "pyproject.toml" + result = BlackRunner().invoke( + black.main, ["--verbose", "--config", str(empty_config), str(path)] + ) + assert result.exit_code == 1 + assert result.stderr_bytes is not None + + gitignore = path / "a" / ".gitignore" + assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + def test_empty_include(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" report = black.Report() @@ -1536,6 +1800,8 @@ class BlackTestCase(BlackBaseTestCase): Path(path / "b/.definitely_exclude/a.pie"), Path(path / "b/.definitely_exclude/a.py"), Path(path / "b/.definitely_exclude/a.pyi"), + Path(path / ".gitignore"), + Path(path / "pyproject.toml"), ] this_abs = THIS_DIR.resolve() sources.extend( @@ -1545,25 +1811,23 @@ class BlackTestCase(BlackBaseTestCase): empty, re.compile(black.DEFAULT_EXCLUDES), None, + None, report, gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) - def test_empty_exclude(self) -> None: + def test_extend_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 = [ - Path(path / "b/dont_exclude/a.py"), - Path(path / "b/dont_exclude/a.pyi"), Path(path / "b/exclude/a.py"), - Path(path / "b/exclude/a.pyi"), - Path(path / "b/.definitely_exclude/a.py"), - Path(path / "b/.definitely_exclude/a.pyi"), + Path(path / "b/dont_exclude/a.py"), ] this_abs = THIS_DIR.resolve() sources.extend( @@ -1571,18 +1835,31 @@ class BlackTestCase(BlackBaseTestCase): path.iterdir(), this_abs, re.compile(black.DEFAULT_INCLUDES), - empty, + re.compile(r"\.pyi$"), + re.compile(r"\.definitely_exclude"), None, report, gitignore, + verbose=False, + quiet=False, ) ) self.assertEqual(sorted(expected), sorted(sources)) - def test_invalid_include_exclude(self) -> None: - for option in ["--include", "--exclude"]: + def test_invalid_cli_regex(self) -> None: + for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) + def test_required_version_matches_version(self) -> None: + self.invokeBlack( + ["--required-version", black.__version__], exit_code=0, ignore_config=True + ) + + def test_required_version_does_not_match_version(self) -> None: + self.invokeBlack( + ["--required-version", "20.99b"], exit_code=1, ignore_config=True + ) + def test_preserves_line_endings(self) -> None: with TemporaryDirectory() as workspace: test_file = Path(workspace) / "test.py" @@ -1603,7 +1880,7 @@ class BlackTestCase(BlackBaseTestCase): black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8")) ) self.assertEqual(result.exit_code, 0) - output = runner.stdout_bytes + output = result.stdout_bytes self.assertIn(nl.encode("utf8"), output) if nl == "\n": self.assertNotIn(b"\r\n", output) @@ -1629,7 +1906,16 @@ class BlackTestCase(BlackBaseTestCase): try: list( black.gen_python_files( - path.iterdir(), root, include, exclude, None, report, gitignore + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, + verbose=False, + quiet=False, ) ) except ValueError as ve: @@ -1643,7 +1929,16 @@ class BlackTestCase(BlackBaseTestCase): with self.assertRaises(ValueError): list( black.gen_python_files( - path.iterdir(), root, include, exclude, None, report, gitignore + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, + verbose=False, + quiet=False, ) ) path.iterdir.assert_called() @@ -1655,7 +1950,7 @@ class BlackTestCase(BlackBaseTestCase): def test_shhh_click(self) -> None: try: - from click import _unicodefun # type: ignore + from click import _unicodefun except ModuleNotFoundError: self.skipTest("Incompatible Click version") if not hasattr(_unicodefun, "_verify_python3_env"): @@ -1664,14 +1959,14 @@ class BlackTestCase(BlackBaseTestCase): with patch("locale.getpreferredencoding") as gpe: gpe.return_value = "ASCII" with self.assertRaises(RuntimeError): - _unicodefun._verify_python3_env() + _unicodefun._verify_python3_env() # type: ignore # Now, let's silence Click... black.patch_click() # ...and confirm it's silent. with patch("locale.getpreferredencoding") as gpe: gpe.return_value = "ASCII" try: - _unicodefun._verify_python3_env() + _unicodefun._verify_python3_env() # type: ignore except RuntimeError as re: self.fail(f"`patch_click()` failed, exception still raised: {re}") @@ -1688,7 +1983,7 @@ class BlackTestCase(BlackBaseTestCase): critical=fail, log=fail, ): - ff(THIS_FILE) + ff(THIS_DIR / "util.py") def test_invalid_config_return_code(self) -> None: tmp_file = Path(black.dump_to_file()) @@ -1748,22 +2043,231 @@ class BlackTestCase(BlackBaseTestCase): self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve()) self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve()) + @patch( + "black.files.find_user_pyproject_toml", + black.files.find_user_pyproject_toml.__wrapped__, + ) + def test_find_user_pyproject_toml_linux(self) -> None: + if system() == "Windows": + return + + # Test if XDG_CONFIG_HOME is checked + with TemporaryDirectory() as workspace: + tmp_user_config = Path(workspace) / "black" + with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}): + self.assertEqual( + black.files.find_user_pyproject_toml(), tmp_user_config.resolve() + ) + + # Test fallback for XDG_CONFIG_HOME + with patch.dict("os.environ"): + os.environ.pop("XDG_CONFIG_HOME", None) + fallback_user_config = Path("~/.config").expanduser() / "black" + self.assertEqual( + black.files.find_user_pyproject_toml(), fallback_user_config.resolve() + ) + + def test_find_user_pyproject_toml_windows(self) -> None: + if system() != "Windows": + return + + user_config_path = Path.home() / ".black" + self.assertEqual( + black.files.find_user_pyproject_toml(), user_config_path.resolve() + ) + def test_bpo_33660_workaround(self) -> None: if system() == "Windows": return # https://bugs.python.org/issue33660 - - old_cwd = Path.cwd() - try: - root = Path("/") - os.chdir(str(root)) + root = Path("/") + with change_directory(root): path = Path("workspace") / "project" report = black.Report(verbose=True) normalized_path = black.normalize_path_maybe_ignore(path, root, report) self.assertEqual(normalized_path, "workspace/project") + + def test_newline_comment_interaction(self) -> None: + source = "class A:\\\r\n# type: ignore\n pass\n" + output = black.format_str(source, mode=DEFAULT_MODE) + black.assert_stable(source, output, mode=DEFAULT_MODE) + + def test_bpo_2142_workaround(self) -> None: + + # https://bugs.python.org/issue2142 + + source, _ = read_data("missing_final_newline.py") + # read_data adds a trailing newline + source = source.rstrip() + expected, _ = read_data("missing_final_newline.diff") + tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False)) + diff_header = re.compile( + rf"{re.escape(str(tmp_file))}\t\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" + ) + try: + result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)]) + self.assertEqual(result.exit_code, 0) finally: - os.chdir(str(old_cwd)) + os.unlink(tmp_file) + actual = result.output + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + self.assertEqual(actual, expected) + + @pytest.mark.python2 + def test_docstring_reformat_for_py27(self) -> None: + """ + Check that stripping trailing whitespace from Python 2 docstrings + doesn't trigger a "not equivalent to source" error + """ + source = ( + b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n' + ) + expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n' + + result = CliRunner().invoke( + black.main, + ["-", "-q", "--target-version=py27"], + input=BytesIO(source), + ) + + self.assertEqual(result.exit_code, 0) + actual = result.output + self.assertFormatEqual(actual, expected) + + @staticmethod + def compare_results( + result: click.testing.Result, expected_value: str, expected_exit_code: int + ) -> None: + """Helper method to test the value and exit code of a click Result.""" + assert ( + result.output == expected_value + ), "The output did not match the expected value." + assert result.exit_code == expected_exit_code, "The exit code is incorrect." + + def test_code_option(self) -> None: + """Test the code option with no changes.""" + code = 'print("Hello world")\n' + args = ["--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, code, 0) + + def test_code_option_changed(self) -> None: + """Test the code option when changes are required.""" + code = "print('hello world')" + formatted = black.format_str(code, mode=DEFAULT_MODE) + + args = ["--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, formatted, 0) + + def test_code_option_check(self) -> None: + """Test the code option when check is passed.""" + args = ["--check", "--code", 'print("Hello world")\n'] + result = CliRunner().invoke(black.main, args) + self.compare_results(result, "", 0) + + def test_code_option_check_changed(self) -> None: + """Test the code option when changes are required, and check is passed.""" + args = ["--check", "--code", "print('hello world')"] + result = CliRunner().invoke(black.main, args) + self.compare_results(result, "", 1) + + def test_code_option_diff(self) -> None: + """Test the code option when diff is passed.""" + code = "print('hello world')" + formatted = black.format_str(code, mode=DEFAULT_MODE) + result_diff = diff(code, formatted, "STDIN", "STDOUT") + + args = ["--diff", "--code", code] + result = CliRunner().invoke(black.main, args) + + # Remove time from diff + output = DIFF_TIME.sub("", result.output) + + assert output == result_diff, "The output did not match the expected value." + assert result.exit_code == 0, "The exit code is incorrect." + + def test_code_option_color_diff(self) -> None: + """Test the code option when color and diff are passed.""" + code = "print('hello world')" + formatted = black.format_str(code, mode=DEFAULT_MODE) + + result_diff = diff(code, formatted, "STDIN", "STDOUT") + result_diff = color_diff(result_diff) + + args = ["--diff", "--color", "--code", code] + result = CliRunner().invoke(black.main, args) + + # Remove time from diff + output = DIFF_TIME.sub("", result.output) + + assert output == result_diff, "The output did not match the expected value." + assert result.exit_code == 0, "The exit code is incorrect." + + def test_code_option_safe(self) -> None: + """Test that the code option throws an error when the sanity checks fail.""" + # Patch black.assert_equivalent to ensure the sanity checks fail + with patch.object(black, "assert_equivalent", side_effect=AssertionError): + code = 'print("Hello world")' + error_msg = f"{code}\nerror: cannot format : \n" + + args = ["--safe", "--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, error_msg, 123) + + def test_code_option_fast(self) -> None: + """Test that the code option ignores errors when the sanity checks fail.""" + # Patch black.assert_equivalent to ensure the sanity checks fail + with patch.object(black, "assert_equivalent", side_effect=AssertionError): + code = 'print("Hello world")' + formatted = black.format_str(code, mode=DEFAULT_MODE) + + args = ["--fast", "--code", code] + result = CliRunner().invoke(black.main, args) + + self.compare_results(result, formatted, 0) + + def test_code_option_config(self) -> None: + """ + Test that the code option finds the pyproject.toml in the current directory. + """ + with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: + args = ["--code", "print"] + CliRunner().invoke(black.main, args) + + pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve() + assert ( + len(parse.mock_calls) >= 1 + ), "Expected config parse to be called with the current directory." + + _, call_args, _ = parse.mock_calls[0] + assert ( + call_args[0].lower() == str(pyproject_path).lower() + ), "Incorrect config loaded." + + def test_code_option_parent_config(self) -> None: + """ + Test that the code option finds the pyproject.toml in the parent directory. + """ + with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: + with change_directory(Path("tests")): + args = ["--code", "print"] + CliRunner().invoke(black.main, args) + + pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve() + assert ( + len(parse.mock_calls) >= 1 + ), "Expected config parse to be called with the current directory." + + _, call_args, _ = parse.mock_calls[0] + assert ( + call_args[0].lower() == str(pyproject_path).lower() + ), "Incorrect config loaded." with open(black.__file__, "r", encoding="utf-8") as _bf: