From fa7163fcb2d6ac13c2f3d2646d97601abe187106 Mon Sep 17 00:00:00 2001 From: Adrien Cossa <21343492+souliane@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:08:05 +0200 Subject: [PATCH] Add option `--do-not-add` to `sync-hooks-additional-dependencies` Closes #8 --- README.md | 8 +- .../sync_hooks_additional_dependencies.py | 52 ++++++++++- ...test_sync_hooks_additional_dependencies.py | 87 +++++++++++++++++++ 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2eb2f75..07682d1 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ repos: # "mypy" is the id of a pre-commit hook # "types" is the name of your poetry group containing typing dependencies # "main" is the automated name associated with the "default" poetry dependencies - args: ["--bind", "mypy=types,main"] + # `--do-not-add` will update or remove dependencies, but not add any new one. + args: ["--bind", "mypy=types,main", "--do-not-add"] ``` ## How it works @@ -76,6 +77,11 @@ look for the version of all the dependencies of these groups in your `poetry.lock`. In `.pre-commit-config.yaml`, it will identify the corresponding hook, and set the `additional_dependencies` key to the list sorted of all the dependencies. +If you pass the option `--do-not-add`, packages that are already in your pre-commit +config file will be updated or removed (if they are not listed in any of the considered +poetry dependencies' groups), but no new packages will be added. You should use +this to avoid installing unecessary dependencies in the pre-commit environment, +e.g. if mypy does not need all of them to type check your project. ## Credit where it's due diff --git a/poetry_to_pre_commit/sync_hooks_additional_dependencies.py b/poetry_to_pre_commit/sync_hooks_additional_dependencies.py index 04a7b21..bdd7914 100644 --- a/poetry_to_pre_commit/sync_hooks_additional_dependencies.py +++ b/poetry_to_pre_commit/sync_hooks_additional_dependencies.py @@ -2,6 +2,7 @@ import argparse import pathlib +import re import sys from typing import Any, Iterable @@ -46,6 +47,11 @@ def get_sync_hooks_additional_dependencies_parser() -> argparse.ArgumentParser: f"`--bind mypy={MAIN_GROUP} --bind mypy:types` or " f"`--bind mypy={MAIN_GROUP},types`).", ) + parser.add_argument( + "--do-not-add", + action="store_true", + help="Update or remove dependencies, but don't add any new one.", + ) return parser @@ -68,12 +74,39 @@ def get_poetry_deps(*, cwd: pathlib.Path | None = None, group: str) -> Iterable[ yield f"{dep.complete_name}=={package.version}" +def update_or_remove_additional_deps( + poetry_deps: set[str], hook_additional_deps: list[str] +) -> set[str]: + # Additional packages that are already in pre-commit configuration could be listed with + # any format that is accepted by pip. The following regex might not cover all the cases. + current_deps = re.findall( + r"^\s*([^=><\[\s]+)", "\n".join(hook_additional_deps), re.MULTILINE + ) + + return { + package + for package in poetry_deps + # package is yielded by `get_poetry_deps` above, and we are pretty sure that this won't raise `IndexError` + if package.split("==")[0].split("[")[0] in current_deps + } + + def sync_hook_additional_deps( *, config: dict[str, Any], deps_by_group: dict[str, list[str]], bind: dict[str, set[str]], + do_not_add: bool = False, ) -> None: + """Sync additional dependencies from `deps_by_group` to `config`. + + Args: + config: pre-commit config + deps_by_group: packages from poetry.lock, by poetry dependency group + bind: poetry dependency groups to consider for each pre-commit hook + do_not_add: Update or remove existing dependencies from the "additional_dependencies" + section of pre-commit config, but do not add new dependencies from poetry. + """ for repo in config.get("repos", []): for hook in repo.get("hooks", []): hook_id = hook["id"] @@ -86,14 +119,19 @@ def sync_hook_additional_deps( for group in groups: deps.update(deps_by_group.get(group, set())) - hook["additional_dependencies"] = sorted(deps) + hook["additional_dependencies"] = sorted( + update_or_remove_additional_deps(deps, hook["additional_dependencies"]) + if do_not_add + else deps + ) def sync_hooks_additional_dependencies( argv: list[str], pre_commit_path: pathlib.Path = PRE_COMMIT_CONFIG_FILE, poetry_cwd: pathlib.Path | None = None, -): +) -> None: + """Sync additional dependencies with the packages versions from poetry lock file.""" parser = get_sync_hooks_additional_dependencies_parser() args = parser.parse_args(argv) @@ -105,8 +143,14 @@ def sync_hooks_additional_dependencies( deps_by_group[group] = set(get_poetry_deps(cwd=poetry_cwd, group=group)) with common.pre_commit_config_roundtrip(pre_commit_path) as config: - sync_hook_additional_deps(config=config, bind=bind, deps_by_group=deps_by_group) + sync_hook_additional_deps( + config=config, + bind=bind, + deps_by_group=deps_by_group, + do_not_add=args.do_not_add, + ) -def sync_hooks_additional_dependencies_cli(): +def sync_hooks_additional_dependencies_cli() -> None: + """Entrypoint when running from the shell.""" sync_hooks_additional_dependencies(argv=sys.argv[1:]) diff --git a/tests/test_sync_hooks_additional_dependencies.py b/tests/test_sync_hooks_additional_dependencies.py index 28c43c5..91bd352 100644 --- a/tests/test_sync_hooks_additional_dependencies.py +++ b/tests/test_sync_hooks_additional_dependencies.py @@ -121,3 +121,90 @@ def test_sync_hooks_additional_dependencies(tmp_path, poetry_cwd): "psycopg[pool]==3.1.18", "types-requests==2.31.0.20240311", ] + + +class TestParamDoNotAdd: + """Test the option `--do-not-add`.""" + + @pytest.mark.parametrize( + ("poetry_deps", "additional_deps", "expected_additional_deps"), + [ + (["a==1", "b"], ["a"], ["a==1"]), + (["a==1", "b"], ["a[x]==2"], ["a==1"]), + (["a[x]==1", "b"], ["a"], ["a[x]==1"]), + (["a[x]==1", "b"], ["a[x]"], ["a[x]==1"]), + (["a==1", "b"], ["a == 2"], ["a==1"]), + (["a==1", "b"], ["a<=2"], ["a==1"]), + (["a==1", "b"], ["a>=1"], ["a==1"]), + ], + ) + def test_sync_hook_additional_deps( + self, poetry_deps, additional_deps, expected_additional_deps + ) -> None: + """Check that `sync_hook_additional_deps` handles the different ways to write a package entry.""" + config = { + "repos": [ + {"hooks": [{"id": "mypy", "additional_dependencies": additional_deps}]} + ] + } + deps_by_group = {"main": poetry_deps} + bind = {"mypy": {"main"}} + + sync_hooks_additional_dependencies.sync_hook_additional_deps( + config=config, deps_by_group=deps_by_group, bind=bind, do_not_add=True + ) + assert config == { + "repos": [ + { + "hooks": [ + { + "id": "mypy", + "additional_dependencies": expected_additional_deps, + } + ] + } + ] + } + + @pytest.mark.parametrize( + ("additional_deps", "expected_additional_deps"), + [ + ([], []), + (["attrs", "psycopg"], ["attrs==23.2.0", "psycopg[pool]==3.1.18"]), + (["attrs", "psycopg[pool]"], ["attrs==23.2.0", "psycopg[pool]==3.1.18"]), + (["attrs", "psycopg[dummy]"], ["attrs==23.2.0", "psycopg[pool]==3.1.18"]), + (["attrs", "fastapi==1.0.0"], ["attrs==23.2.0"]), + ], + ) + def test_sync_hooks_additional_dependencies( + self, tmp_path, poetry_cwd, additional_deps, expected_additional_deps + ) -> None: + pre_commit_path = tmp_path / ".pre-commit-config.yaml" + ruamel.yaml.YAML().dump( + { + "repos": [ + { + "repo": "https://github.com/foo/pyright-python", + "rev": "v1.1.300", + "hooks": [ + { + "id": "pyright", + "additional_dependencies": additional_deps, + } + ], + } + ] + }, + pre_commit_path, + ) + + sync_hooks_additional_dependencies.sync_hooks_additional_dependencies( + argv=["foo", "--bind", "pyright=types,main", "--do-not-add"], + pre_commit_path=pre_commit_path, + poetry_cwd=poetry_cwd, + ) + result = ruamel.yaml.YAML().load(pre_commit_path.read_text()) + assert ( + result["repos"][0]["hooks"][0]["additional_dependencies"] + == expected_additional_deps + )