diff --git a/README.md b/README.md index f3e0c63..21355a2 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,17 @@ dependency-mapping = {"package-name"= {"repo"= "https://github.com/example/packa > Note: the `dependency-mapping` is merged with the default mapping, so you don't need to specify the default mapping if you want to add a new mapping. > Repos urls will be normalized to http(s), with the trailing slash removed. +### From environment + +Some settings are overridable by environment variables with the following `SYNC_PRE_COMMIT_LOCK_*` prefixed environement variables: + +| `toml` setting | environment | format | +| -----------------------------|----------------------------------------|-----------------------------------| +| `automaticall-install-hooks` | `SYNC_PRE_COMMIT_LOCK_INSTALL` | `bool` as string (`true`, `1`...) | +| `disable-sync-from-lock` | `SYNC_PRE_COMMIT_LOCK_DISABLED` | `bool` as string (`true`, `1`...) | +| `ignore` | `SYNC_PRE_COMMIT_LOCK_IGNORE` | coma-seprated list | +| `pre-commit-config-file` | `SYNC_PRE_COMMIT_LOCK_PRE_COMMIT_FILE` | `str` | + ## Usage Once installed, and optionally configured, the plugin usage should be transparent, and trigger when you run applicable PDM or Poetry commands, like `pdm lock`, or `poetry lock`. diff --git a/src/sync_pre_commit_lock/config.py b/src/sync_pre_commit_lock/config.py index bd8f819..e05c3c6 100644 --- a/src/sync_pre_commit_lock/config.py +++ b/src/sync_pre_commit_lock/config.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any @@ -17,6 +18,22 @@ pass +def env_as_bool(value: str) -> bool: + return (value or "False").lower() in ("true", "1") + + +def env_as_list(value: str) -> list[str]: + return [v.strip() for v in (value or "").split(",")] + + +ENV_PREFIX = "SYNC_PRE_COMMIT_LOCK" +ENV_CAST = { + "DISABLED": env_as_bool, + "IGNORE": env_as_list, + "INSTALL": env_as_bool, +} + + def from_toml(data: dict[str, Any]) -> SyncPreCommitLockConfig: fields = {f.metadata.get("toml", f.name): f for f in SyncPreCommitLockConfig.__dataclass_fields__.values()} # XXX We should warn about unknown fields @@ -33,12 +50,27 @@ def from_toml(data: dict[str, Any]) -> SyncPreCommitLockConfig: ) +def update_from_env(config: SyncPreCommitLockConfig) -> SyncPreCommitLockConfig: + vars = { + f.metadata["env"]: f for f in SyncPreCommitLockConfig.__dataclass_fields__.values() if f.metadata.get("env") + } + for var, specs in vars.items(): + if value := os.getenv(f"{ENV_PREFIX}_{var}"): + caster = ENV_CAST.get(var, lambda v: v) + setattr(config, specs.name, caster(value)) + return config + + @dataclass class SyncPreCommitLockConfig: - automatically_install_hooks: bool = field(default=True, metadata={"toml": "automatically-install-hooks"}) - disable_sync_from_lock: bool = field(default=False, metadata={"toml": "disable-sync-from-lock"}) - ignore: list[str] = field(default_factory=list, metadata={"toml": "ignore"}) - pre_commit_config_file: str = field(metadata={"toml": "pre-commit-config-file"}, default=".pre-commit-config.yaml") + automatically_install_hooks: bool = field( + default=True, metadata={"toml": "automatically-install-hooks", "env": "INSTALL"} + ) + disable_sync_from_lock: bool = field(default=False, metadata={"toml": "disable-sync-from-lock", "env": "DISABLED"}) + ignore: list[str] = field(default_factory=list, metadata={"toml": "ignore", "env": "IGNORE"}) + pre_commit_config_file: str = field( + metadata={"toml": "pre-commit-config-file", "env": "PRE_COMMIT_FILE"}, default=".pre-commit-config.yaml" + ) dependency_mapping: PackageRepoMapping = field(default_factory=dict, metadata={"toml": "dependency-mapping"}) @@ -52,4 +84,4 @@ def load_config(path: Path | None = None) -> SyncPreCommitLockConfig: if not tool_dict or len(tool_dict) == 0: return SyncPreCommitLockConfig() - return from_toml(tool_dict) + return update_from_env(from_toml(tool_dict)) diff --git a/tests/test_config.py b/tests/test_config.py index 8c7cbc1..b0dbe3e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch -from sync_pre_commit_lock.config import SyncPreCommitLockConfig, from_toml, load_config +import pytest +from sync_pre_commit_lock.config import SyncPreCommitLockConfig, from_toml, load_config, update_from_env from sync_pre_commit_lock.db import RepoInfo @@ -23,6 +24,24 @@ def test_from_toml() -> None: assert actual_config == expected_config +def test_update_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("SYNC_PRE_COMMIT_LOCK_DISABLED", "1") + monkeypatch.setenv("SYNC_PRE_COMMIT_LOCK_INSTALL", "false") + monkeypatch.setenv("SYNC_PRE_COMMIT_LOCK_IGNORE", "a, b") + monkeypatch.setenv("SYNC_PRE_COMMIT_LOCK_PRE_COMMIT_FILE", ".test-config.yaml") + expected_config = SyncPreCommitLockConfig( + automatically_install_hooks=False, + disable_sync_from_lock=True, + ignore=["a", "b"], + pre_commit_config_file=".test-config.yaml", + dependency_mapping={}, + ) + + actual_config = update_from_env(SyncPreCommitLockConfig()) + + assert actual_config == expected_config + + def test_sync_pre_commit_lock_config() -> None: config = SyncPreCommitLockConfig( disable_sync_from_lock=True, @@ -63,3 +82,20 @@ def test_load_config_with_data(mock_from_toml: MagicMock, mock_open: MagicMock, mock_path.open.assert_called_once_with("rb") mock_load.assert_called_once() mock_from_toml.assert_called_once_with({"disable": True}) + + +@patch("sync_pre_commit_lock.config.toml.load", return_value={"tool": {"sync-pre-commit-lock": {"ignore": ["fake"]}}}) +@patch("builtins.open", new_callable=MagicMock) +def test_env_override_config(mock_open: MagicMock, mock_load: MagicMock, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("SYNC_PRE_COMMIT_LOCK_DISABLED", "true") + monkeypatch.setenv("SYNC_PRE_COMMIT_LOCK_IGNORE", "a, b") + expected_config = SyncPreCommitLockConfig( + disable_sync_from_lock=True, + ignore=["a", "b"], + ) + mock_path = MagicMock() + mock_path.open = mock_open(read_data="dummy_stream") + actual_config = load_config(mock_path) + + assert actual_config == expected_config + mock_path.open.assert_called_once_with("rb")