Skip to content

Commit

Permalink
Add option --do-not-add to sync-hooks-additional-dependencies
Browse files Browse the repository at this point in the history
Closes #8
  • Loading branch information
souliane committed Apr 10, 2024
1 parent 7d0d726 commit fa7163f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 5 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
52 changes: 48 additions & 4 deletions poetry_to_pre_commit/sync_hooks_additional_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import pathlib
import re
import sys
from typing import Any, Iterable

Expand Down Expand Up @@ -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


Expand All @@ -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"]
Expand All @@ -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)

Expand All @@ -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:])
87 changes: 87 additions & 0 deletions tests/test_sync_hooks_additional_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

0 comments on commit fa7163f

Please sign in to comment.