Skip to content

Commit

Permalink
feat(config): allow overriding config from environment (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
noirbizarre committed Dec 22, 2023
1 parent 45a5eb7 commit d72b7ca
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 8 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 environment variables:

| `toml` setting | environment | format |
| ------------------------------|----------------------------------------|-----------------------------------|
| `automatically-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` | comma-separated 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`.
Expand Down
62 changes: 55 additions & 7 deletions src/sync_pre_commit_lock/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Callable, TypedDict

try:
# 3.11+
Expand All @@ -16,6 +17,16 @@

pass

ENV_PREFIX = "SYNC_PRE_COMMIT_LOCK"


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(",")]


def from_toml(data: dict[str, Any]) -> SyncPreCommitLockConfig:
fields = {f.metadata.get("toml", f.name): f for f in SyncPreCommitLockConfig.__dataclass_fields__.values()}
Expand All @@ -33,13 +44,50 @@ 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 = specs.metadata.get("cast", lambda v: v)
setattr(config, specs.name, caster(value))
return config


class Metadata(TypedDict, total=False):
"""Configuration metadata known fields"""

toml: str
"""Map the `toml` field"""
env: str
"""Optionnaly map the environment variable suffix"""
cast: Callable[[str], Any]
"""Optionnaly provide a cast function for environment variable"""


@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")
dependency_mapping: PackageRepoMapping = field(default_factory=dict, metadata={"toml": "dependency-mapping"})
automatically_install_hooks: bool = field(
default=True,
metadata=Metadata(toml="automatically-install-hooks", env="INSTALL", cast=env_as_bool),
)
disable_sync_from_lock: bool = field(
default=False,
metadata=Metadata(toml="disable-sync-from-lock", env="DISABLED", cast=env_as_bool),
)
ignore: list[str] = field(
default_factory=list,
metadata=Metadata(toml="ignore", env="IGNORE", cast=env_as_list),
)
pre_commit_config_file: str = field(
default=".pre-commit-config.yaml",
metadata=Metadata(toml="pre-commit-config-file", env="PRE_COMMIT_FILE"),
)
dependency_mapping: PackageRepoMapping = field(
default_factory=dict,
metadata=Metadata(toml="dependency-mapping"),
)


def load_config(path: Path | None = None) -> SyncPreCommitLockConfig:
Expand All @@ -52,4 +100,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))
38 changes: 37 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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,
Expand Down Expand Up @@ -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")

0 comments on commit d72b7ca

Please sign in to comment.