Skip to content

Commit

Permalink
Merge pull request #11 from souliane/main
Browse files Browse the repository at this point in the history
  • Loading branch information
ewjoachim authored May 3, 2024
2 parents 1dd97ec + 33a16f0 commit e7e4850
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 9 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"]
# `--no-new-deps` will update or remove dependencies, but not add any new one.
args: ["--bind", "mypy=types,main", "--no-new-deps"]
```
## 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 `--no-new-deps`, 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 can 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
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 50 additions & 5 deletions poetry_to_pre_commit/sync_hooks_additional_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
from typing import Any, Iterable

from packaging.requirements import Requirement
from poetry import factory
from poetry.core.packages.dependency_group import MAIN_GROUP

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(
"--no-new-deps",
action="store_true",
help="Update or remove dependencies, but don't add any new one.",
)
return parser


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


def sync_hook_additional_deps(
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 - use `Requirement` to parse them properly.
current_deps = [Requirement(dep).name for dep in hook_additional_deps]

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_hooks_additional_dependencies(
*,
config: dict[str, Any],
deps_by_group: dict[str, list[str]],
bind: dict[str, set[str]],
no_new_deps: 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
no_new_deps: 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 +117,22 @@ 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(
poetry_deps=deps,
hook_additional_deps=hook["additional_dependencies"],
)
if no_new_deps
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 +144,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_hooks_additional_dependencies(
config=config,
bind=bind,
deps_by_group=deps_by_group,
no_new_deps=args.no_new_deps,
)


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:])
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ sync-hooks-additional-dependencies = 'poetry_to_pre_commit.__init__:sync_hooks_a
python = "^3.8"
poetry = "*"
"ruamel.yaml" = "*"
packaging = "^24.0"

[tool.poetry.group.types]
optional = true
Expand Down
89 changes: 87 additions & 2 deletions tests/test_sync_hooks_additional_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ def test_get_poetry_deps__error(poetry_cwd):
)


def test_sync_hook_additional_deps():
def test__sync_hooks_additional_dependencies():
config = {"repos": [{"hooks": [{"id": "mypy"}, {"id": "foo"}]}]}
deps_by_group = {
"types": ["bar==1", "baz[e]==2"],
"main": ["qux==3"],
}
bind = {"mypy": {"types", "main", "unknown"}, "other_unknown": {"abc"}}
sync_hooks_additional_dependencies.sync_hook_additional_deps(
sync_hooks_additional_dependencies._sync_hooks_additional_dependencies(
config=config,
deps_by_group=deps_by_group,
bind=bind,
Expand Down Expand Up @@ -121,3 +121,88 @@ def test_sync_hooks_additional_dependencies(tmp_path, poetry_cwd):
"psycopg[pool]==3.1.18",
"types-requests==2.31.0.20240311",
]


@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_hooks_additional_deps__no_new_deps(
poetry_deps, additional_deps, expected_additional_deps
) -> None:
"""Check that `_sync_hooks_additional_dependencies` 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_hooks_additional_dependencies(
config=config, deps_by_group=deps_by_group, bind=bind, no_new_deps=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__no_new_deps(
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", "--no-new-deps"],
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 e7e4850

Please sign in to comment.