Skip to content
This repository was archived by the owner on Jul 12, 2025. It is now read-only.

Commit e7e4850

Browse files
authored
Merge pull request #11 from souliane/main
2 parents 1dd97ec + 33a16f0 commit e7e4850

File tree

5 files changed

+146
-9
lines changed

5 files changed

+146
-9
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ repos:
4747
# "mypy" is the id of a pre-commit hook
4848
# "types" is the name of your poetry group containing typing dependencies
4949
# "main" is the automated name associated with the "default" poetry dependencies
50-
args: ["--bind", "mypy=types,main"]
50+
# `--no-new-deps` will update or remove dependencies, but not add any new one.
51+
args: ["--bind", "mypy=types,main", "--no-new-deps"]
5152
```
5253
5354
## How it works
@@ -76,6 +77,11 @@ look for the version of all the dependencies of these groups in your
7677
`poetry.lock`. In `.pre-commit-config.yaml`, it will identify the corresponding
7778
hook, and set the `additional_dependencies` key to the list sorted of all the
7879
dependencies.
80+
If you pass the option `--no-new-deps`, packages that are already in your pre-commit
81+
config file will be updated or removed (if they are not listed in any of the considered
82+
poetry dependencies' groups), but no new packages will be added. You can use
83+
this to avoid installing unecessary dependencies in the pre-commit environment,
84+
e.g. if mypy does not need all of them to type check your project.
7985

8086
## Credit where it's due
8187

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

poetry_to_pre_commit/sync_hooks_additional_dependencies.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
from typing import Any, Iterable
77

8+
from packaging.requirements import Requirement
89
from poetry import factory
910
from poetry.core.packages.dependency_group import MAIN_GROUP
1011

@@ -46,6 +47,11 @@ def get_sync_hooks_additional_dependencies_parser() -> argparse.ArgumentParser:
4647
f"`--bind mypy={MAIN_GROUP} --bind mypy:types` or "
4748
f"`--bind mypy={MAIN_GROUP},types`).",
4849
)
50+
parser.add_argument(
51+
"--no-new-deps",
52+
action="store_true",
53+
help="Update or remove dependencies, but don't add any new one.",
54+
)
4955
return parser
5056

5157

@@ -68,12 +74,37 @@ def get_poetry_deps(*, cwd: pathlib.Path | None = None, group: str) -> Iterable[
6874
yield f"{dep.complete_name}=={package.version}"
6975

7076

71-
def sync_hook_additional_deps(
77+
def update_or_remove_additional_deps(
78+
poetry_deps: set[str], hook_additional_deps: list[str]
79+
) -> set[str]:
80+
# Additional packages that are already in pre-commit configuration could be listed with
81+
# any format that is accepted by pip - use `Requirement` to parse them properly.
82+
current_deps = [Requirement(dep).name for dep in hook_additional_deps]
83+
84+
return {
85+
package
86+
for package in poetry_deps
87+
# package is yielded by `get_poetry_deps` above, and we are pretty sure that this won't raise `IndexError`
88+
if package.split("==")[0].split("[")[0] in current_deps
89+
}
90+
91+
92+
def _sync_hooks_additional_dependencies(
7293
*,
7394
config: dict[str, Any],
7495
deps_by_group: dict[str, list[str]],
7596
bind: dict[str, set[str]],
97+
no_new_deps: bool = False,
7698
) -> None:
99+
"""Sync additional dependencies from `deps_by_group` to `config`.
100+
101+
Args:
102+
config: pre-commit config
103+
deps_by_group: packages from poetry.lock, by poetry dependency group
104+
bind: poetry dependency groups to consider for each pre-commit hook
105+
no_new_deps: Update or remove existing dependencies from the "additional_dependencies"
106+
section of pre-commit config, but do not add new dependencies from poetry.
107+
"""
77108
for repo in config.get("repos", []):
78109
for hook in repo.get("hooks", []):
79110
hook_id = hook["id"]
@@ -86,14 +117,22 @@ def sync_hook_additional_deps(
86117
for group in groups:
87118
deps.update(deps_by_group.get(group, set()))
88119

89-
hook["additional_dependencies"] = sorted(deps)
120+
hook["additional_dependencies"] = sorted(
121+
update_or_remove_additional_deps(
122+
poetry_deps=deps,
123+
hook_additional_deps=hook["additional_dependencies"],
124+
)
125+
if no_new_deps
126+
else deps
127+
)
90128

91129

92130
def sync_hooks_additional_dependencies(
93131
argv: list[str],
94132
pre_commit_path: pathlib.Path = PRE_COMMIT_CONFIG_FILE,
95133
poetry_cwd: pathlib.Path | None = None,
96-
):
134+
) -> None:
135+
"""Sync additional dependencies with the packages versions from poetry lock file."""
97136
parser = get_sync_hooks_additional_dependencies_parser()
98137
args = parser.parse_args(argv)
99138

@@ -105,8 +144,14 @@ def sync_hooks_additional_dependencies(
105144
deps_by_group[group] = set(get_poetry_deps(cwd=poetry_cwd, group=group))
106145

107146
with common.pre_commit_config_roundtrip(pre_commit_path) as config:
108-
sync_hook_additional_deps(config=config, bind=bind, deps_by_group=deps_by_group)
147+
_sync_hooks_additional_dependencies(
148+
config=config,
149+
bind=bind,
150+
deps_by_group=deps_by_group,
151+
no_new_deps=args.no_new_deps,
152+
)
109153

110154

111-
def sync_hooks_additional_dependencies_cli():
155+
def sync_hooks_additional_dependencies_cli() -> None:
156+
"""Entrypoint when running from the shell."""
112157
sync_hooks_additional_dependencies(argv=sys.argv[1:])

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ sync-hooks-additional-dependencies = 'poetry_to_pre_commit.__init__:sync_hooks_a
2525
python = "^3.8"
2626
poetry = "*"
2727
"ruamel.yaml" = "*"
28+
packaging = "^24.0"
2829

2930
[tool.poetry.group.types]
3031
optional = true

tests/test_sync_hooks_additional_dependencies.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ def test_get_poetry_deps__error(poetry_cwd):
6363
)
6464

6565

66-
def test_sync_hook_additional_deps():
66+
def test__sync_hooks_additional_dependencies():
6767
config = {"repos": [{"hooks": [{"id": "mypy"}, {"id": "foo"}]}]}
6868
deps_by_group = {
6969
"types": ["bar==1", "baz[e]==2"],
7070
"main": ["qux==3"],
7171
}
7272
bind = {"mypy": {"types", "main", "unknown"}, "other_unknown": {"abc"}}
73-
sync_hooks_additional_dependencies.sync_hook_additional_deps(
73+
sync_hooks_additional_dependencies._sync_hooks_additional_dependencies(
7474
config=config,
7575
deps_by_group=deps_by_group,
7676
bind=bind,
@@ -121,3 +121,88 @@ def test_sync_hooks_additional_dependencies(tmp_path, poetry_cwd):
121121
"psycopg[pool]==3.1.18",
122122
"types-requests==2.31.0.20240311",
123123
]
124+
125+
126+
@pytest.mark.parametrize(
127+
("poetry_deps", "additional_deps", "expected_additional_deps"),
128+
[
129+
(["a==1", "b"], ["a"], ["a==1"]),
130+
(["a==1", "b"], ["a[x]==2"], ["a==1"]),
131+
(["a[x]==1", "b"], ["a"], ["a[x]==1"]),
132+
(["a[x]==1", "b"], ["a[x]"], ["a[x]==1"]),
133+
(["a==1", "b"], ["a == 2"], ["a==1"]),
134+
(["a==1", "b"], ["a<=2"], ["a==1"]),
135+
(["a==1", "b"], ["a>=1"], ["a==1"]),
136+
],
137+
)
138+
def test__sync_hooks_additional_deps__no_new_deps(
139+
poetry_deps, additional_deps, expected_additional_deps
140+
) -> None:
141+
"""Check that `_sync_hooks_additional_dependencies` handles the different ways to write a package entry."""
142+
config = {
143+
"repos": [
144+
{"hooks": [{"id": "mypy", "additional_dependencies": additional_deps}]}
145+
]
146+
}
147+
deps_by_group = {"main": poetry_deps}
148+
bind = {"mypy": {"main"}}
149+
150+
sync_hooks_additional_dependencies._sync_hooks_additional_dependencies(
151+
config=config, deps_by_group=deps_by_group, bind=bind, no_new_deps=True
152+
)
153+
assert config == {
154+
"repos": [
155+
{
156+
"hooks": [
157+
{
158+
"id": "mypy",
159+
"additional_dependencies": expected_additional_deps,
160+
}
161+
]
162+
}
163+
]
164+
}
165+
166+
167+
@pytest.mark.parametrize(
168+
("additional_deps", "expected_additional_deps"),
169+
[
170+
([], []),
171+
(["attrs", "psycopg"], ["attrs==23.2.0", "psycopg[pool]==3.1.18"]),
172+
(["attrs", "psycopg[pool]"], ["attrs==23.2.0", "psycopg[pool]==3.1.18"]),
173+
(["attrs", "psycopg[dummy]"], ["attrs==23.2.0", "psycopg[pool]==3.1.18"]),
174+
(["attrs", "fastapi==1.0.0"], ["attrs==23.2.0"]),
175+
],
176+
)
177+
def test_sync_hooks_additional_dependencies__no_new_deps(
178+
tmp_path, poetry_cwd, additional_deps, expected_additional_deps
179+
) -> None:
180+
pre_commit_path = tmp_path / ".pre-commit-config.yaml"
181+
ruamel.yaml.YAML().dump(
182+
{
183+
"repos": [
184+
{
185+
"repo": "https://github.com/foo/pyright-python",
186+
"rev": "v1.1.300",
187+
"hooks": [
188+
{
189+
"id": "pyright",
190+
"additional_dependencies": additional_deps,
191+
}
192+
],
193+
}
194+
]
195+
},
196+
pre_commit_path,
197+
)
198+
199+
sync_hooks_additional_dependencies.sync_hooks_additional_dependencies(
200+
argv=["foo", "--bind", "pyright=types,main", "--no-new-deps"],
201+
pre_commit_path=pre_commit_path,
202+
poetry_cwd=poetry_cwd,
203+
)
204+
result = ruamel.yaml.YAML().load(pre_commit_path.read_text())
205+
assert (
206+
result["repos"][0]["hooks"][0]["additional_dependencies"]
207+
== expected_additional_deps
208+
)

0 commit comments

Comments
 (0)