diff --git a/pyproject.toml b/pyproject.toml index cbd3b3b..08b55c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ skip_covered = true [tool.mypy] python_version = "3.8" -# strict = true +strict = true ignore_missing_imports = true [tool.thx] diff --git a/ufmt/__init__.py b/ufmt/__init__.py index abfe153..56d0bbe 100644 --- a/ufmt/__init__.py +++ b/ufmt/__init__.py @@ -23,6 +23,8 @@ ) __all__ = [ + "__author__", + "__version__", "BlackConfig", "BlackConfigFactory", "Encoding", diff --git a/ufmt/cli.py b/ufmt/cli.py index ab525ed..9a535eb 100644 --- a/ufmt/cli.py +++ b/ufmt/cli.py @@ -10,8 +10,8 @@ from moreorless.click import echo_color_precomputed_diff from .__version__ import __version__ -from .core import Result, ufmt_paths -from .types import Options +from .core import ufmt_paths +from .types import Options, Result from .util import enable_libcst_native @@ -113,7 +113,7 @@ def main( debug: Optional[bool], concurrency: Optional[int], root: Optional[Path], -): +) -> None: init_logging(debug=debug) ctx.obj = Options( @@ -130,7 +130,7 @@ def main( @click.argument( "names", type=click.Path(allow_dash=True), nargs=-1, metavar="[PATH] ..." ) -def check(ctx: click.Context, names: List[str]): +def check(ctx: click.Context, names: List[str]) -> None: """Check formatting of one or more paths""" options: Options = ctx.obj paths = [Path(name) for name in names] if names else [Path(".")] @@ -147,7 +147,7 @@ def check(ctx: click.Context, names: List[str]): @click.argument( "names", type=click.Path(allow_dash=True), nargs=-1, metavar="[PATH] ..." ) -def diff(ctx: click.Context, names: List[str]): +def diff(ctx: click.Context, names: List[str]) -> None: """Generate diffs for any files that need formatting""" options: Options = ctx.obj paths = [Path(name) for name in names] if names else [Path(".")] @@ -168,7 +168,7 @@ def diff(ctx: click.Context, names: List[str]): @click.argument( "names", type=click.Path(allow_dash=True), nargs=-1, metavar="[PATH] ..." ) -def format(ctx: click.Context, names: List[str]): +def format(ctx: click.Context, names: List[str]) -> None: """Format one or more paths in place""" options: Options = ctx.obj paths = [Path(name) for name in names] if names else [Path(".")] diff --git a/ufmt/core.py b/ufmt/core.py index 5a25376..c3ca096 100644 --- a/ufmt/core.py +++ b/ufmt/core.py @@ -14,7 +14,8 @@ import black import black.mode -from moreorless.click import unified_diff +import black.report +from moreorless import unified_diff from trailrunner import Trailrunner from usort import usort @@ -123,7 +124,7 @@ def ufmt_bytes( fast=False, mode=black_config, ) - except black.NothingChanged: + except black.report.NothingChanged: content = result.output elif ufmt_config.formatter == Formatter.ruff_api: options = ruff_api.FormatOptions( @@ -346,7 +347,7 @@ def ufmt_stdin( if result.diff and path != STDIN: real_path_str = path.as_posix() - def replacement(match: Match) -> str: + def replacement(match: Match[str]) -> str: return match.group(1) + real_path_str pattern = re.compile(r"^((?:---|\+\+\+)\s+).+$", re.M) diff --git a/ufmt/tests/cli.py b/ufmt/tests/cli.py index 1d67cf6..2989e8a 100644 --- a/ufmt/tests/cli.py +++ b/ufmt/tests/cli.py @@ -7,34 +7,34 @@ from pathlib import Path from tempfile import TemporaryDirectory from unittest import skipIf, TestCase -from unittest.mock import call, patch +from unittest.mock import call, Mock, patch import trailrunner from click.testing import CliRunner from libcst import ParserSyntaxError from ufmt.cli import echo_results, main -from ufmt.core import Result +from ufmt.types import Result from .core import CORRECTLY_FORMATTED_CODE, POORLY_FORMATTED_CODE @patch.object(trailrunner.core.Trailrunner, "DEFAULT_EXECUTOR", ThreadPoolExecutor) class CliTest(TestCase): - def setUp(self): + def setUp(self) -> None: self.runner = CliRunner(mix_stderr=False) self.cwd = os.getcwd() self.td = TemporaryDirectory() self.tdp = Path(self.td.name).resolve() os.chdir(self.tdp) - def tearDown(self): + def tearDown(self) -> None: os.chdir(self.cwd) self.td.cleanup() @patch("ufmt.cli.echo_color_precomputed_diff") @patch("ufmt.cli.click.secho") - def test_echo(self, echo_mock, mol_mock): + def test_echo(self, echo_mock: Mock, mol_mock: Mock) -> None: f1 = Path("foo/bar.py") f2 = Path("fuzz/buzz.py") f3 = Path("make/rake.py") @@ -100,7 +100,7 @@ def test_echo(self, echo_mock, mol_mock): mol_mock.reset_mock() @patch("ufmt.cli.ufmt_paths") - def test_check(self, ufmt_mock): + def test_check(self, ufmt_mock: Mock) -> None: with self.subTest("no paths given"): ufmt_mock.reset_mock() ufmt_mock.return_value = [] @@ -180,7 +180,7 @@ def test_check(self, ufmt_mock): self.assertEqual(0, result.exit_code) @patch("ufmt.cli.ufmt_paths") - def test_diff(self, ufmt_mock): + def test_diff(self, ufmt_mock: Mock) -> None: with self.subTest("no paths given"): ufmt_mock.reset_mock() ufmt_mock.return_value = [] @@ -289,7 +289,7 @@ def test_diff(self, ufmt_mock): self.assertEqual(2, result.exit_code) @patch("ufmt.cli.ufmt_paths") - def test_format(self, ufmt_mock): + def test_format(self, ufmt_mock: Mock) -> None: with self.subTest("no paths given"): ufmt_mock.reset_mock() ufmt_mock.return_value = [] @@ -418,14 +418,14 @@ def test_stdin(self) -> None: self.assertIn("Formatted hello.py\n", result.stderr) self.assertEqual(0, result.exit_code) - def test_end_to_end(self): + def test_end_to_end(self) -> None: alpha = self.tdp / "alpha.py" beta = self.tdp / "beta.py" (self.tdp / "sub").mkdir() gamma = self.tdp / "sub" / "gamma.py" kappa = self.tdp / "sub" / "kappa.py" - def reset(): + def reset() -> None: alpha.write_text(CORRECTLY_FORMATTED_CODE) beta.write_text(POORLY_FORMATTED_CODE) gamma.write_text(CORRECTLY_FORMATTED_CODE) diff --git a/ufmt/tests/config.py b/ufmt/tests/config.py index 07a9f97..8a1945c 100644 --- a/ufmt/tests/config.py +++ b/ufmt/tests/config.py @@ -1,32 +1,35 @@ # Copyright 2022 Amethyst Reese # Licensed under the MIT license +from contextlib import AbstractContextManager from pathlib import Path from tempfile import TemporaryDirectory from textwrap import dedent +from typing import Any from unittest import TestCase -from unittest.mock import ANY, patch +from unittest.mock import ANY, Mock, patch from trailrunner.tests.core import cd -from ufmt.config import load_config, UfmtConfig +from ufmt.config import load_config +from ufmt.types import UfmtConfig class ConfigTest(TestCase): maxDiff = None - def setUp(self): + def setUp(self) -> None: self._td = TemporaryDirectory() self.addCleanup(self._td.cleanup) self.td = Path(self._td.name).resolve() self.pyproject = self.td / "pyproject.toml" - def subTest(self, *args, **kwargs): + def subTest(self, *args: Any, **kwargs: Any) -> AbstractContextManager[None]: load_config.cache_clear() return super().subTest(*args, **kwargs) - def test_ufmt_config(self): + def test_ufmt_config(self) -> None: fake_config = dedent( """ [tool.ufmt] @@ -111,7 +114,7 @@ def test_ufmt_config(self): ) @patch("ufmt.config.LOG") - def test_invalid_config(self, log_mock): + def test_invalid_config(self, log_mock: Mock) -> None: with self.subTest("string"): self.pyproject.write_text( dedent( @@ -190,7 +193,7 @@ def test_invalid_config(self, log_mock): load_config(self.td / "fake.py") @patch("ufmt.config.LOG") - def test_config_excludes(self, log_mock): + def test_config_excludes(self, log_mock: Mock) -> None: with self.subTest("missing"): self.pyproject.write_text( dedent( diff --git a/ufmt/tests/core.py b/ufmt/tests/core.py index 805e1ff..aa8b111 100644 --- a/ufmt/tests/core.py +++ b/ufmt/tests/core.py @@ -119,7 +119,7 @@ class CoreTest(TestCase): @patch("ufmt.core.black.format_file_contents", wraps=black.format_file_contents) @patch("ufmt.core.ruff_api.format_string", wraps=ruff_api.format_string) - def test_ufmt_bytes(self, ruff_mock, black_mock): + def test_ufmt_bytes(self, ruff_mock: Mock, black_mock: Mock) -> None: black_config = BlackConfig() usort_config = UsortConfig() @@ -161,7 +161,9 @@ def test_ufmt_bytes(self, ruff_mock, black_mock): @patch("ufmt.core.black.format_file_contents", wraps=black.format_file_contents) @patch("ufmt.core.ruff_api.format_string", wraps=ruff_api.format_string) - def test_ufmt_bytes_alternate_formatter(self, ruff_mock, black_mock): + def test_ufmt_bytes_alternate_formatter( + self, ruff_mock: Mock, black_mock: Mock + ) -> None: black_config = BlackConfig() usort_config = UsortConfig() @@ -211,7 +213,7 @@ def test_ufmt_bytes_alternate_formatter(self, ruff_mock, black_mock): ufmt.ufmt_bytes( Path("foo.pyi"), POORLY_FORMATTED_STUB.encode(), - ufmt_config=UfmtConfig(formatter="garbage"), + ufmt_config=UfmtConfig(formatter="garbage"), # type:ignore black_config=black_config, usort_config=usort_config, ) @@ -219,7 +221,7 @@ def test_ufmt_bytes_alternate_formatter(self, ruff_mock, black_mock): black_mock.assert_not_called() @patch("ufmt.core.usort", wraps=usort.usort) - def test_ufmt_bytes_alternate_sorter(self, usort_mock): + def test_ufmt_bytes_alternate_sorter(self, usort_mock: Mock) -> None: black_config = BlackConfig() usort_config = UsortConfig() @@ -267,13 +269,13 @@ def test_ufmt_bytes_alternate_sorter(self, usort_mock): ufmt.ufmt_bytes( Path("foo.py"), POORLY_FORMATTED_CODE.encode(), - ufmt_config=UfmtConfig(sorter="garbage"), + ufmt_config=UfmtConfig(sorter="garbage"), # type:ignore black_config=black_config, usort_config=usort_config, ) usort_mock.assert_not_called() - def test_ufmt_bytes_pre_processor(self): + def test_ufmt_bytes_pre_processor(self) -> None: def pre_processor( path: Path, content: bytes, *, encoding: Encoding = "utf-8" ) -> bytes: @@ -294,7 +296,7 @@ def pre_processor( CORRECTLY_FORMATTED_CODE.encode() + b'\n\nprint("hello")\n', result ) - def test_ufmt_bytes_post_processor(self): + def test_ufmt_bytes_post_processor(self) -> None: def post_processor( path: Path, content: bytes, *, encoding: Encoding = "utf-8" ) -> bytes: @@ -315,7 +317,7 @@ def post_processor( CORRECTLY_FORMATTED_CODE.encode() + b"\nprint('hello')\n", result ) - def test_ufmt_string(self): + def test_ufmt_string(self) -> None: black_config = BlackConfig() usort_config = UsortConfig() @@ -353,10 +355,10 @@ def test_ufmt_string(self): with self.subTest("version check"): self.assertRegex(ufmt.__version__, r"^2\.", "remove ufmt_string in 3.0") - def test_ufmt_file(self): + def test_ufmt_file(self) -> None: with TemporaryDirectory() as td: - td = Path(td) - f = td / "foo.py" + tdp = Path(td) + f = tdp / "foo.py" f.write_text(POORLY_FORMATTED_CODE) with self.subTest("dry run"): @@ -452,7 +454,7 @@ def skip_with_reason( @patch("ufmt.core.sys.stdin") @patch("ufmt.core.sys.stdout") - def test_ufmt_stdin(self, stdout_mock, stdin_mock): + def test_ufmt_stdin(self, stdout_mock: Mock, stdin_mock: Mock) -> None: with self.subTest("check"): stdin_mock.buffer = stdin = io.BytesIO() stdout_mock.buffer = stdout = io.BytesIO() @@ -489,7 +491,7 @@ def test_ufmt_stdin(self, stdout_mock, stdin_mock): result = ufmt_stdin(path, dry_run=True, diff=True) self.assertIsNotNone(result.diff) self.assertRegex( - result.diff, r"--- hello.world\.py\n\+\+\+ hello.world\.py" + result.diff or "", r"--- hello.world\.py\n\+\+\+ hello.world\.py" ) stdout.seek(0) self.assertEqual(b"", stdout.read()) @@ -506,11 +508,11 @@ def test_ufmt_stdin(self, stdout_mock, stdin_mock): stdout.seek(0) self.assertEqual(CORRECTLY_FORMATTED_CODE.encode(), stdout.read()) - def test_ufmt_paths(self): + def test_ufmt_paths(self) -> None: with TemporaryDirectory() as td: - td = Path(td) - f1 = td / "bar.py" - sd = td / "foo" + tdp = Path(td) + f1 = tdp / "bar.py" + sd = tdp / "foo" sd.mkdir() f2 = sd / "baz.py" f3 = sd / "frob.py" @@ -528,7 +530,7 @@ def test_ufmt_paths(self): with self.subTest("non-existent paths"): results = list( ufmt.ufmt_paths( - [(td / "fake.py"), (td / "another.py")], dry_run=True + [(tdp / "fake.py"), (tdp / "another.py")], dry_run=True ) ) self.assertEqual([], results) @@ -638,7 +640,7 @@ def test_ufmt_paths(self): file_wrapper.reset_mock() @patch("ufmt.core.ufmt_stdin") - def test_ufmt_paths_stdin(self, stdin_mock): + def test_ufmt_paths_stdin(self, stdin_mock: Mock) -> None: stdin_mock.return_value = Result(path=STDIN, changed=True) with self.subTest("no name"): @@ -679,13 +681,18 @@ def test_ufmt_paths_stdin(self, stdin_mock): @patch("ufmt.core.sys.stdin") @patch("ufmt.core.sys.stdout") - def test_ufmt_paths_stdin_resolves(self, stdout_mock, stdin_mock): + def test_ufmt_paths_stdin_resolves( + self, stdout_mock: Mock, stdin_mock: Mock + ) -> None: stdin_mock.buffer = io.BytesIO(POORLY_FORMATTED_CODE.encode()) stdout_mock.buffer = stdout = io.BytesIO() - def fake_preprocessor(path, content, encoding): + def fake_preprocessor( + path: Path, content: FileContent, encoding: Encoding + ) -> FileContent: # ensure the fake path resolves correctly (#94) path.resolve() + return content result = list(ufmt.ufmt_paths([STDIN])) expected = [Result(Path("stdin"), changed=True, written=True)] @@ -695,10 +702,10 @@ def fake_preprocessor(path, content, encoding): self.assertListEqual(expected, result) self.assertEqual(CORRECTLY_FORMATTED_CODE.encode(), output) - def test_ufmt_paths_config(self): + def test_ufmt_paths_config(self) -> None: with TemporaryDirectory() as td: - td = Path(td).resolve() - md = td / "foo" + tdp = Path(td).resolve() + md = tdp / "foo" md.mkdir() f1 = md / "__init__.py" f2 = md / "foo.py" @@ -709,12 +716,12 @@ def test_ufmt_paths_config(self): for f in f1, f2, f3: f.write_text(POORLY_FORMATTED_CODE) - pyproj = td / "pyproject.toml" + pyproj = tdp / "pyproject.toml" pyproj.write_text(FAKE_CONFIG) file_wrapper = Mock(name="ufmt_file", wraps=ufmt.ufmt_file) with patch("ufmt.core.ufmt_file", file_wrapper): - list(ufmt.ufmt_paths([td])) + list(ufmt.ufmt_paths([tdp])) file_wrapper.assert_has_calls( [ call( @@ -735,7 +742,7 @@ def test_ufmt_paths_config(self): self.assertEqual(f2.read_text(), CORRECTLY_FORMATTED_CODE) self.assertEqual(f3.read_text(), POORLY_FORMATTED_CODE) - def test_e2e_empty_files(self): + def test_e2e_empty_files(self) -> None: with TemporaryDirectory() as td: tdp = Path(td).resolve() foo = tdp / "foo.py" @@ -753,10 +760,10 @@ def test_e2e_empty_files(self): ] self.assertListEqual(expected, results) - def test_e2e_return_bytes(self): + def test_e2e_return_bytes(self) -> None: with TemporaryDirectory() as td: - td = Path(td).resolve() - foo = td / "foo.py" + tdp = Path(td).resolve() + foo = tdp / "foo.py" with self.subTest("unix newlines"): foo.write_bytes(POORLY_FORMATTED_CODE.encode()) diff --git a/ufmt/tests/util.py b/ufmt/tests/util.py index 0858d5e..efe74b4 100644 --- a/ufmt/tests/util.py +++ b/ufmt/tests/util.py @@ -3,10 +3,11 @@ from pathlib import Path from tempfile import TemporaryDirectory +from typing import cast from unittest import TestCase import tomlkit -from black import TargetVersion +from black.mode import TargetVersion import ufmt from .core import FAKE_CONFIG, POORLY_FORMATTED_CODE @@ -20,52 +21,41 @@ def bar(): class UtilTest(TestCase): - def test_black_config(self): - black_config = dict( - target_version=["py36", "py37"], - skip_string_normalization=True, - line_length=87, - ) - + def test_black_config(self) -> None: doc = tomlkit.parse(FAKE_CONFIG) black = tomlkit.table() - for key, value in black_config.items(): - black[key] = value - doc["tool"].add("black", black) + black["target_version"] = ["py36", "py37"] + black["skip_string_normalization"] = True + black["line_length"] = 87 + cast(tomlkit.container.Container, doc["tool"]).add("black", black) with TemporaryDirectory() as td: - td = Path(td) + tdp = Path(td) - pyproj = td / "pyproject.toml" + pyproj = tdp / "pyproject.toml" pyproj.write_text(tomlkit.dumps(doc)) - f = td / "foo.py" + f = tdp / "foo.py" f.write_text(POORLY_FORMATTED_CODE) result = ufmt.ufmt_file(f, dry_run=True) self.assertTrue(result.changed) - mode = ufmt.util.make_black_config(td) + mode = ufmt.util.make_black_config(tdp) with self.subTest("target_versions"): self.assertEqual( mode.target_versions, - { - TargetVersion[item.upper()] - for item in black_config["target_version"] - }, + {TargetVersion[item.upper()] for item in ("py36", "py37")}, ) with self.subTest("string_normalization"): - self.assertIs( - mode.string_normalization, - not black_config["skip_string_normalization"], - ) + self.assertFalse(mode.string_normalization) with self.subTest("line_length"): - self.assertEqual(mode.line_length, black_config["line_length"]) + self.assertEqual(mode.line_length, 87) - def test_read_file(self): + def test_read_file(self) -> None: with TemporaryDirectory() as td: tdp = Path(td).resolve() foo = tdp / "foo.py" diff --git a/ufmt/types.py b/ufmt/types.py index fb5f4df..9af7cbd 100644 --- a/ufmt/types.py +++ b/ufmt/types.py @@ -10,6 +10,21 @@ from typing_extensions import Protocol from usort import Config as UsortConfig +__all__ = [ + "BlackConfig", + "BlackConfigFactory", + "Encoding", + "FileContent", + "Newline", + "Options", + "Processor", + "Result", + "SkipFormatting", + "STDIN", + "UsortConfig", + "UsortConfigFactory", +] + STDIN = Path("-") Encoding = str diff --git a/ufmt/util.py b/ufmt/util.py index 043ba5e..9f80fcf 100644 --- a/ufmt/util.py +++ b/ufmt/util.py @@ -6,7 +6,8 @@ from pathlib import Path from typing import Tuple -from black import find_pyproject_toml, parse_pyproject_toml, TargetVersion +from black.files import find_pyproject_toml, parse_pyproject_toml +from black.mode import TargetVersion from .types import BlackConfig, Encoding, FileContent, Newline @@ -27,10 +28,7 @@ def make_black_config(path: Path) -> BlackConfig: } config["string_normalization"] = not config.pop("skip_string_normalization", False) - names = { - field.name - for field in BlackConfig.__dataclass_fields__.values() # type: ignore[attr-defined] - } + names = {field.name for field in BlackConfig.__dataclass_fields__.values()} config = {name: value for name, value in config.items() if name in names} return BlackConfig(**config)