8000 feat(config): enforce required habit-hooks version by tayfuryldz · Pull Request #196 · habit-hooks/habit-hooks · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ requires-python = ">=3.11"
dependencies = [
"jinja2>=3",
"pathspec>=0.12",
"packaging>=24",
"attrs>=23",
"jq>=1.8",
"habit-hooks-generic>=1.5.dev0,<2",
Expand Down Expand Up @@ -98,8 +99,6 @@ habit-hooks-ruby = { workspace = true }
dev = [
"pytest>=8",
"markdown-it-py>=3",
# The floor gate asks pip's own question of this repo's own specifiers.
"packaging>=24",
"ruff>=0.15.20",
"deptry>=0.25.1",
"habit-hooks-generic",
Expand Down
21 changes: 21 additions & 0 deletions src/habit_hooks/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
from __future__ import annotations

from pathlib import Path
from importlib.metadata import version

from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.version import Version

from .cli import ConfigError

from .config_schema import (
PLUGIN_CONFIG_KEYS,
Expand Down Expand Up @@ -110,9 +116,24 @@ def declared_detectors(plugins: list[str], project_dir: Path) -> list[Detector]:
return _plugin_detectors(plugins, _plugin_configs(plugins, project_dir))


def _validate_requires(requirement: str | None) -> None:
if requirement is None:
return
if not isinstance(requirement, str):
raise ConfigError("'requires' in the project config must be a version requirement string")
try:
required = SpecifierSet(requirement)
except InvalidSpecifier:
raise ConfigError(f"invalid 'requires' version requirement {requirement!r}") from None
running = Version(version("habit-hooks"))
if running not in required:
raise ConfigError(f"project requires habit-hooks {requirement}, but running version is {running}")


def load_config(project_dir: Path, config_path: Path | None = None) -> Config:
path = config_path or project_config_path(project_dir)
config = _build_config(_read_toml(path))
_validate_requires(config.requires)
plugin_configs = _plugin_configs(config.plugins, project_dir)
if config.files is None:
config.files = _plugin_files(plugin_configs) or None
Expand Down
1 change: 1 addition & 0 deletions src/habit_hooks/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class SensorOverride:

@define
class Config:
requires: str | None = None
plugins: list[str] = field(factory=lambda: ["generic"])
transformers: list[str] = field(factory=lambda: ["snooze"])
files: list[str] | None = None
Expand Down
22 changes: 22 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
from __future__ import annotations

from pathlib import Path
from importlib.metadata import version

import pytest

from habit_hooks.cli import ConfigError

from habit_hooks.config import (
Config,
Expand Down Expand Up @@ -117,3 +122,20 @@ def test_direct_defaults_are_independent_instances() -> None:
a.plugins.append("mutated")
assert b.plugins == ["generic"]
assert a.scope is not b.scope


def test_satisfied_requires_version_loads(tmp_path: Path) -> None:
running = version("habit-hooks")
assert _load(_write(tmp_path, f'requires = "=={running}"\n')).requires == f"=={running}"


def test_unmet_requires_version_names_requirement_and_running_version(tmp_path: Path) -> None:
with pytest.raises(ConfigError) as refusal:
_load(_write(tmp_path, 'requires = ">=9999"\n'))
assert ">=9999" in str(refusal.value)
assert version("habit-hooks") in str(refusal.value)


def test_invalid_requires_version_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="invalid 'requires' version requirement"):
_load(_write(tmp_path, 'requires = "definitely not a specifier"\n'))
Loading
0