Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): allow overriding config from environment #10

Merged
merged 1 commit into from
Dec 22, 2023
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
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")
Loading