Public Access
DEVX-61: feat: single-source-of-truth config via [tool.devx] in pyproject.toml
Post-merge / detect-type (push) Successful in 6s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 12s
Post-merge / release (push) Failing after 48s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 48s
Post-merge / detect-type (push) Successful in 6s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 12s
Post-merge / release (push) Failing after 48s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 48s
This commit was merged in pull request #99.
This commit is contained in:
@@ -27,7 +27,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 5 --max-single-seconds 0.5
|
||||
- name: Documentation coverage check
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
|
||||
@@ -96,6 +96,13 @@ strict = ["src/devx/config.py", "src/devx/exceptions.py", "src/devx/i18n.py", "s
|
||||
# Rule priority (first match wins):
|
||||
# 1. user_facing_overrides (safety — highest priority)
|
||||
# 2. infrastructure_overrides (explicit per-file)
|
||||
# Project-specific devx configuration (read by devx.config)
|
||||
[tool.devx]
|
||||
task_prefix = "DEVX"
|
||||
vikunja_project_id = 8
|
||||
repo_owner = "oblachno-oss"
|
||||
repo_name = "devx"
|
||||
|
||||
# 3. infrastructure (DEFAULT_INFRASTRUCTURE + project-specific patterns)
|
||||
# 4. Default: user-facing (safe)
|
||||
[tool.devx.classify]
|
||||
|
||||
+65
-7
@@ -1,28 +1,86 @@
|
||||
"""Shared configuration constants for devx scripts and API clients.
|
||||
|
||||
All defaults can be overridden via environment variables with the ``DEVX_``
|
||||
prefix. Projects consuming devx can set these in their ``.env`` files.
|
||||
Configuration is read from two sources, in priority order:
|
||||
|
||||
1. **Environment variables** (``DEVX_`` prefix) — highest priority, used for
|
||||
CI secrets and per-run overrides.
|
||||
2. **``[tool.devx]`` section in ``pyproject.toml``** — project defaults,
|
||||
read from the current working directory.
|
||||
|
||||
If neither source provides a value, built-in defaults are used.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_pyproject_devx() -> dict[str, object]:
|
||||
"""Load the ``[tool.devx]`` section from pyproject.toml in the CWD.
|
||||
|
||||
Returns an empty dict if the file or section is missing.
|
||||
"""
|
||||
path = Path("pyproject.toml")
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(path, "rb") as f: # noqa: PTH123
|
||||
data: dict[str, object] = tomllib.load(f)
|
||||
except (tomllib.TOMLDecodeError, OSError):
|
||||
return {}
|
||||
tool_raw: object = data.get("tool", {})
|
||||
if not isinstance(tool_raw, dict):
|
||||
return {}
|
||||
tool: dict[str, object] = tool_raw # type: ignore[assignment]
|
||||
devx_raw: object = tool.get("devx", {})
|
||||
if not isinstance(devx_raw, dict):
|
||||
return {}
|
||||
devx: dict[str, object] = devx_raw # type: ignore[assignment]
|
||||
return devx
|
||||
|
||||
|
||||
_PYPROJECT = _load_pyproject_devx()
|
||||
|
||||
|
||||
def _get(key: str, env_var: str, default: str) -> str:
|
||||
"""Get a config value: env var > pyproject.toml > default."""
|
||||
env_val = os.getenv(env_var)
|
||||
if env_val is not None:
|
||||
return env_val
|
||||
pyproject_val = _PYPROJECT.get(key)
|
||||
if isinstance(pyproject_val, str):
|
||||
return pyproject_val
|
||||
return default
|
||||
|
||||
|
||||
def _get_int(key: str, env_var: str, default: int) -> int:
|
||||
"""Get an int config value: env var > pyproject.toml > default."""
|
||||
env_val = os.getenv(env_var)
|
||||
if env_val is not None:
|
||||
return int(env_val)
|
||||
pyproject_val = _PYPROJECT.get(key)
|
||||
if isinstance(pyproject_val, int):
|
||||
return pyproject_val
|
||||
return default
|
||||
|
||||
|
||||
# API endpoints — override via env vars for different Gitea/Vikunja instances
|
||||
GITEA_API_URL = os.getenv("DEVX_GITEA_API_URL", "https://git.oblachno.oblachno.fyi/api/v1")
|
||||
VIKUNJA_API_URL = os.getenv("DEVX_VIKUNJA_API_URL", "https://work.oblachno.oblachno.fyi/api/v1")
|
||||
GITEA_API_URL = _get("gitea_api_url", "DEVX_GITEA_API_URL", "https://git.oblachno.oblachno.fyi/api/v1")
|
||||
VIKUNJA_API_URL = _get("vikunja_api_url", "DEVX_VIKUNJA_API_URL", "https://work.oblachno.oblachno.fyi/api/v1")
|
||||
|
||||
# Organization defaults — each project MUST set DEVX_REPO_OWNER explicitly.
|
||||
# No default: prevents silent 404s when the wrong owner is used.
|
||||
REPO_OWNER = os.getenv("DEVX_REPO_OWNER", "")
|
||||
REPO_OWNER = _get("repo_owner", "DEVX_REPO_OWNER", "")
|
||||
|
||||
# Task prefix for Vikunja task IDs — each project sets its own (GRM, DEVX, INFRA, etc.)
|
||||
TASK_PREFIX = os.getenv("DEVX_TASK_PREFIX", "DEVX")
|
||||
TASK_PREFIX = _get("task_prefix", "DEVX_TASK_PREFIX", "DEVX")
|
||||
TASK_ID_RE = re.compile(rf"{TASK_PREFIX}-\d+")
|
||||
|
||||
# Vikunja project ID — each project uses a different Vikunja project
|
||||
VIKUNJA_PROJECT_ID = int(os.getenv("DEVX_VIKUNJA_PROJECT_ID", "6"))
|
||||
VIKUNJA_PROJECT_ID = _get_int("vikunja_project_id", "DEVX_VIKUNJA_PROJECT_ID", 6)
|
||||
|
||||
# HTTP client defaults
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
+20
-24
@@ -2,15 +2,16 @@
|
||||
#
|
||||
# This fragment provides common targets for Vikunja task management,
|
||||
# PR creation, and pushing. It is designed to be included from a
|
||||
# project's Makefile after project-specific variables are set.
|
||||
# project's Makefile.
|
||||
#
|
||||
# Project config (task prefix, Vikunja project ID, repo owner, repo name)
|
||||
# is read from [tool.devx] in pyproject.toml by devx.config — no
|
||||
# Makefile variables needed.
|
||||
#
|
||||
# Usage in your Makefile:
|
||||
#
|
||||
# # Set project-specific variables
|
||||
# DEVX_VIKUNJA_PROJECT_ID := 3
|
||||
# DEVX_REPO_OWNER := oblachno
|
||||
# DEVX_REPO_NAME := infra
|
||||
# DEVX_PYTHON := python3 # or $(BIN)/python, etc.
|
||||
# # Set DEVX_PYTHON if you need a specific interpreter
|
||||
# DEVX_PYTHON := $(BIN)/python
|
||||
#
|
||||
# # Include the devx fragment (silent if devx not installed yet)
|
||||
# DEVX_MAK := $(shell $(DEVX_PYTHON) -c \
|
||||
@@ -18,39 +19,34 @@
|
||||
# 2>/dev/null)
|
||||
# -include $(DEVX_MAK)
|
||||
#
|
||||
# The fragment uses ?= for all variables so projects can override them
|
||||
# before the include. If devx is not installed, the -include silently
|
||||
# skips and the targets are simply unavailable (run 'make setup' first).
|
||||
# If devx is not installed, the -include silently skips and the targets
|
||||
# are simply unavailable (run 'make setup' first).
|
||||
#
|
||||
# Variables:
|
||||
# DEVX_VIKUNJA_PROJECT_ID — Vikunja project ID (default: 1)
|
||||
# DEVX_REPO_OWNER — Gitea repository owner (default: empty)
|
||||
# DEVX_REPO_NAME — Gitea repository name (default: empty)
|
||||
# DEVX_PYTHON — Python executable (default: python3)
|
||||
# DEVX_PR_BASE — PR base branch (default: master)
|
||||
# DEVX_PYTHON — Python executable (default: python3)
|
||||
# DEVX_PR_BASE — PR base branch (default: master)
|
||||
|
||||
DEVX_VIKUNJA_PROJECT_ID ?= 1
|
||||
DEVX_REPO_OWNER ?=
|
||||
DEVX_REPO_NAME ?=
|
||||
DEVX_PYTHON ?= python3
|
||||
DEVX_PR_BASE ?= master
|
||||
|
||||
.PHONY: devx-create-task devx-create-pr devx-push devx-push-with-pr
|
||||
.PHONY: devx-create-task devx-create-pr devx-push devx-push-with-pr devx-check-config
|
||||
|
||||
# Create a Vikunja task in the configured project
|
||||
# Create a Vikunja task (project ID read from [tool.devx] in pyproject.toml)
|
||||
devx-create-task:
|
||||
@$(DEVX_PYTHON) -m devx.tools.create_task --project-id $(DEVX_VIKUNJA_PROJECT_ID)
|
||||
@$(DEVX_PYTHON) -m devx.tools.create_task
|
||||
|
||||
# Create a PR with title auto-derived from the Vikunja task
|
||||
# (owner/repo read from [tool.devx] in pyproject.toml)
|
||||
devx-create-pr:
|
||||
@$(DEVX_PYTHON) -m devx.tools.create_pr \
|
||||
--owner $(DEVX_REPO_OWNER) \
|
||||
--repo $(DEVX_REPO_NAME) \
|
||||
--base $(DEVX_PR_BASE)
|
||||
@$(DEVX_PYTHON) -m devx.tools.create_pr --base $(DEVX_PR_BASE)
|
||||
|
||||
# Push current branch to origin
|
||||
devx-push:
|
||||
@git push -u origin HEAD
|
||||
|
||||
# Validate devx configuration in pyproject.toml
|
||||
devx-check-config:
|
||||
@$(DEVX_PYTHON) -m devx.tools.check_config
|
||||
|
||||
# Push and create PR in one step
|
||||
devx-push-with-pr: devx-push devx-create-pr
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate devx configuration consistency in pyproject.toml.
|
||||
|
||||
Checks:
|
||||
1. [tool.devx] section exists with required keys (task_prefix, vikunja_project_id, repo_owner, repo_name)
|
||||
2. devx version is consistent across all extras that mention it
|
||||
|
||||
Usage::
|
||||
|
||||
python3 -m devx.tools.check_config
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from devx.i18n import _
|
||||
|
||||
|
||||
@click.command()
|
||||
def cli() -> None:
|
||||
"""Validate devx configuration in pyproject.toml."""
|
||||
path = Path("pyproject.toml")
|
||||
if not path.exists():
|
||||
click.echo(_("pyproject.toml not found in current directory."))
|
||||
sys.exit(1)
|
||||
|
||||
with open(path, "rb") as f: # noqa: PTH123
|
||||
data = tomllib.load(f)
|
||||
|
||||
errors: list[str] = []
|
||||
|
||||
# Check [tool.devx] section
|
||||
devx_cfg = data.get("tool", {}).get("devx", {})
|
||||
required_keys = {"task_prefix", "vikunja_project_id", "repo_owner", "repo_name"}
|
||||
missing = required_keys - set(devx_cfg.keys())
|
||||
if missing:
|
||||
errors.append(
|
||||
_("[tool.devx] missing required keys: {keys}", keys=", ".join(sorted(missing))),
|
||||
)
|
||||
|
||||
# Check devx version consistency across extras
|
||||
optional_deps = data.get("project", {}).get("optional-dependencies", {})
|
||||
devx_versions: dict[str, str] = {}
|
||||
for extra_name, deps in optional_deps.items():
|
||||
for dep in deps:
|
||||
# Match "devx>=X.Y.Z", "devx==X.Y.Z", "devx>X.Y.Z", etc.
|
||||
m = re.search(r"\bdevx\s*(>=|==|>|<=|<|~=)\s*([\d.]+)", dep)
|
||||
if m:
|
||||
devx_versions[extra_name] = m.group(2)
|
||||
|
||||
if devx_versions:
|
||||
unique_versions = set(devx_versions.values())
|
||||
if len(unique_versions) > 1:
|
||||
detail = ", ".join(f"{extra}={v}" for extra, v in sorted(devx_versions.items()))
|
||||
errors.append(
|
||||
_("devx version mismatch across extras: {detail}", detail=detail),
|
||||
)
|
||||
|
||||
if errors:
|
||||
for err in errors:
|
||||
click.echo(f"ERROR: {err}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(_("Configuration OK: [tool.devx] present, devx versions consistent."))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
cli() # pragma: no cover
|
||||
@@ -439,6 +439,14 @@
|
||||
"ru": "Настройка параметров репозитория...",
|
||||
"zh": "正在配置仓库设置..."
|
||||
},
|
||||
"Configuration OK: [tool.devx] present, devx versions consistent.": {
|
||||
"bg": "Конфигурацията е OK: [tool.devx] присъства, версиите на devx са консистентни.",
|
||||
"de": "Konfiguration OK: [tool.devx] vorhanden, devx-Versionen konsistent.",
|
||||
"en": "Configuration OK: [tool.devx] present, devx versions consistent.",
|
||||
"pl": "Konfiguracja OK: [tool.devx] obecne, wersje devx spójne.",
|
||||
"ru": "Конфигурация OK: [tool.devx] присутствует, версии devx согласованы.",
|
||||
"zh": "配置正常: [tool.devx] 已存在, devx 版本一致。"
|
||||
},
|
||||
"Could not extract conventional commit message from PR commits.": {
|
||||
"bg": "Could not extract conventional commit message from PR commits.",
|
||||
"de": "Could not extract conventional commit message from PR commits.",
|
||||
@@ -487,6 +495,14 @@
|
||||
"ru": "Created release commit.",
|
||||
"zh": "Created release commit."
|
||||
},
|
||||
"devx version mismatch across extras: {detail}": {
|
||||
"bg": "несъответствие на версията на devx между extras: {detail}",
|
||||
"de": "devx-Versionskonflikt zwischen Extras: {detail}",
|
||||
"en": "devx version mismatch across extras: {detail}",
|
||||
"pl": "niezgodność wersji devx między extras: {detail}",
|
||||
"ru": "несоответствие версии devx между extras: {detail}",
|
||||
"zh": "devx 版本在 extras 之间不一致: {detail}"
|
||||
},
|
||||
"Docker daemon already running": {
|
||||
"bg": "Докер демонът вече работи",
|
||||
"de": "Docker-Daemon läuft bereits",
|
||||
@@ -1303,6 +1319,14 @@
|
||||
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
|
||||
},
|
||||
"[tool.devx] missing required keys: {keys}": {
|
||||
"bg": "[tool.devx] липсват задължителни ключове: {keys}",
|
||||
"de": "[tool.devx] fehlt erforderliche Schlüssel: {keys}",
|
||||
"en": "[tool.devx] missing required keys: {keys}",
|
||||
"pl": "[tool.devx] brak wymaganych kluczy: {keys}",
|
||||
"ru": "[tool.devx] отсутствуют обязательные ключи: {keys}",
|
||||
"zh": "[tool.devx] 缺少必需的键: {keys}"
|
||||
},
|
||||
"[dry-run] Would commit: release: v{version}": {
|
||||
"bg": "[dry-run] Would commit: release: v{version}",
|
||||
"de": "[dry-run] Would commit: release: v{version}",
|
||||
@@ -1455,6 +1479,14 @@
|
||||
"ru": "ожидает",
|
||||
"zh": "待处理"
|
||||
},
|
||||
"pyproject.toml not found in current directory.": {
|
||||
"bg": "pyproject.toml не е намерен в текущата директория.",
|
||||
"de": "pyproject.toml im aktuellen Verzeichnis nicht gefunden.",
|
||||
"en": "pyproject.toml not found in current directory.",
|
||||
"pl": "nie znaleziono pyproject.toml w bieżącym katalogu.",
|
||||
"ru": "pyproject.toml не найден в текущей директории.",
|
||||
"zh": "在当前目录中未找到 pyproject.toml。"
|
||||
},
|
||||
"unknown": {
|
||||
"bg": "неизвестен",
|
||||
"de": "unbekannt",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Unit tests for devx.tools.check_config."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from devx.tools.check_config import cli
|
||||
|
||||
|
||||
class TestCheckConfig:
|
||||
def test_valid_config(self, tmp_path: Path) -> None:
|
||||
"""A valid [tool.devx] section with consistent versions passes."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)) as fs:
|
||||
Path(fs, "pyproject.toml").write_text(
|
||||
'[project]\nname = "test"\n'
|
||||
'[project.optional-dependencies]\nci = ["devx>=0.15.0"]\ndev = ["devx>=0.15.0"]\n'
|
||||
'[tool.devx]\ntask_prefix = "TEST"\nvikunja_project_id = 1\nrepo_owner = "owner"\nrepo_name = "test"\n'
|
||||
)
|
||||
result = runner.invoke(cli)
|
||||
assert result.exit_code == 0
|
||||
assert "Configuration OK" in result.output
|
||||
|
||||
def test_missing_tool_devx_section(self, tmp_path: Path) -> None:
|
||||
"""Missing [tool.devx] section fails with error."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)) as fs:
|
||||
Path(fs, "pyproject.toml").write_text('[project]\nname = "test"\n')
|
||||
result = runner.invoke(cli)
|
||||
assert result.exit_code == 1
|
||||
assert "missing required keys" in result.output
|
||||
|
||||
def test_partial_tool_devx_section(self, tmp_path: Path) -> None:
|
||||
"""Partial [tool.devx] section fails with missing keys."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)) as fs:
|
||||
Path(fs, "pyproject.toml").write_text('[project]\nname = "test"\n[tool.devx]\ntask_prefix = "TEST"\n')
|
||||
result = runner.invoke(cli)
|
||||
assert result.exit_code == 1
|
||||
assert "missing required keys" in result.output
|
||||
assert "vikunja_project_id" in result.output
|
||||
assert "repo_owner" in result.output
|
||||
assert "repo_name" in result.output
|
||||
|
||||
def test_version_mismatch(self, tmp_path: Path) -> None:
|
||||
"""Version mismatch across extras fails."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)) as fs:
|
||||
Path(fs, "pyproject.toml").write_text(
|
||||
'[project]\nname = "test"\n'
|
||||
"[project.optional-dependencies]\n"
|
||||
'ci = ["devx>=0.15.0"]\n'
|
||||
'dev = ["devx>=0.14.2"]\n'
|
||||
'[tool.devx]\ntask_prefix = "TEST"\nvikunja_project_id = 1\nrepo_owner = "owner"\nrepo_name = "test"\n'
|
||||
)
|
||||
result = runner.invoke(cli)
|
||||
assert result.exit_code == 1
|
||||
assert "version mismatch" in result.output
|
||||
|
||||
def test_no_pyproject_file(self, tmp_path: Path) -> None:
|
||||
"""Missing pyproject.toml fails."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)):
|
||||
result = runner.invoke(cli)
|
||||
assert result.exit_code == 1
|
||||
assert "not found" in result.output
|
||||
|
||||
def test_no_extras_passes(self, tmp_path: Path) -> None:
|
||||
"""No optional-dependencies with devx is fine (no versions to compare)."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)) as fs:
|
||||
Path(fs, "pyproject.toml").write_text(
|
||||
'[project]\nname = "test"\n'
|
||||
'[tool.devx]\ntask_prefix = "TEST"\nvikunja_project_id = 1\nrepo_owner = "owner"\nrepo_name = "test"\n'
|
||||
)
|
||||
result = runner.invoke(cli)
|
||||
assert result.exit_code == 0
|
||||
assert "Configuration OK" in result.output
|
||||
|
||||
def test_single_extra_passes(self, tmp_path: Path) -> None:
|
||||
"""Single extra with devx version is fine (no mismatch possible)."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)) as fs:
|
||||
Path(fs, "pyproject.toml").write_text(
|
||||
'[project]\nname = "test"\n'
|
||||
'[project.optional-dependencies]\nci = ["devx>=0.15.0", "pytest"]\n'
|
||||
'[tool.devx]\ntask_prefix = "TEST"\nvikunja_project_id = 1\nrepo_owner = "owner"\nrepo_name = "test"\n'
|
||||
)
|
||||
result = runner.invoke(cli)
|
||||
assert result.exit_code == 0
|
||||
+84
-25
@@ -1,12 +1,14 @@
|
||||
"""Unit tests for config module constants."""
|
||||
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
from devx.config import (
|
||||
CONVENTIONAL_RE,
|
||||
DEFAULT_PER_PAGE,
|
||||
DEFAULT_TIMEOUT,
|
||||
GITEA_API_URL,
|
||||
MAX_RETRIES,
|
||||
REPO_OWNER,
|
||||
RETRY_BACKOFF_BASE,
|
||||
RETRY_STATUS_CODES,
|
||||
TASK_ID_RE,
|
||||
@@ -20,25 +22,10 @@ class TestConfigConstants:
|
||||
assert "api/v1" in GITEA_API_URL
|
||||
assert "api/v1" in VIKUNJA_API_URL
|
||||
|
||||
def test_project_ids(self, monkeypatch: object) -> None:
|
||||
"""VIKUNJA_PROJECT_ID defaults to 6 when DEVX_VIKUNJA_PROJECT_ID is not set."""
|
||||
monkeypatch.delenv("DEVX_VIKUNJA_PROJECT_ID", raising=False)
|
||||
import importlib
|
||||
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.VIKUNJA_PROJECT_ID == 6
|
||||
# Restore module state
|
||||
importlib.reload(cfg)
|
||||
|
||||
def test_timeouts(self) -> None:
|
||||
assert DEFAULT_TIMEOUT == 30
|
||||
assert DEFAULT_PER_PAGE == 50
|
||||
|
||||
def test_owner(self) -> None:
|
||||
assert REPO_OWNER == ""
|
||||
|
||||
def test_task_prefix(self) -> None:
|
||||
assert TASK_PREFIX == "DEVX"
|
||||
|
||||
@@ -64,28 +51,100 @@ class TestConfigConstants:
|
||||
assert 503 in RETRY_STATUS_CODES
|
||||
assert 504 in RETRY_STATUS_CODES
|
||||
|
||||
def test_env_var_override(self, monkeypatch: object) -> None:
|
||||
"""Test that env vars override defaults at import time."""
|
||||
# We can't easily re-import the module, but we can verify
|
||||
# the constants respect env vars by checking the module source.
|
||||
|
||||
class TestPyprojectReading:
|
||||
"""Test that config.py reads [tool.devx] from pyproject.toml."""
|
||||
|
||||
def test_pyproject_provides_values(self) -> None:
|
||||
"""When pyproject.toml has [tool.devx], values are read from it."""
|
||||
import devx.config as cfg
|
||||
|
||||
assert cfg.GITEA_API_URL # always non-empty
|
||||
assert cfg.VIKUNJA_API_URL # always non-empty
|
||||
# devx's own pyproject.toml has task_prefix=DEVX, vikunja_project_id=8
|
||||
assert cfg.TASK_PREFIX == "DEVX"
|
||||
assert cfg.VIKUNJA_PROJECT_ID == 8
|
||||
assert cfg.REPO_OWNER == "oblachno-oss"
|
||||
|
||||
def test_env_overrides_pyproject(self, monkeypatch: object) -> None:
|
||||
"""Env vars take priority over pyproject.toml."""
|
||||
monkeypatch.setenv("DEVX_TASK_PREFIX", "CUSTOM")
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.TASK_PREFIX == "CUSTOM"
|
||||
assert cfg.TASK_ID_RE.search("CUSTOM-42")
|
||||
monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False)
|
||||
importlib.reload(cfg)
|
||||
|
||||
def test_no_pyproject_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||
"""When no pyproject.toml exists, defaults are used."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False)
|
||||
monkeypatch.delenv("DEVX_VIKUNJA_PROJECT_ID", raising=False)
|
||||
monkeypatch.delenv("DEVX_REPO_OWNER", raising=False)
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.TASK_PREFIX == "DEVX"
|
||||
assert cfg.VIKUNJA_PROJECT_ID == 6
|
||||
assert cfg.REPO_OWNER == ""
|
||||
importlib.reload(cfg)
|
||||
|
||||
def test_invalid_toml_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||
"""When pyproject.toml is invalid TOML, defaults are used."""
|
||||
(tmp_path / "pyproject.toml").write_text("invalid toml {{{")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False)
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.TASK_PREFIX == "DEVX"
|
||||
importlib.reload(cfg)
|
||||
|
||||
def test_no_devx_section_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||
"""When pyproject.toml has no [tool.devx], defaults are used."""
|
||||
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"\n')
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False)
|
||||
monkeypatch.delenv("DEVX_VIKUNJA_PROJECT_ID", raising=False)
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.TASK_PREFIX == "DEVX"
|
||||
assert cfg.VIKUNJA_PROJECT_ID == 6
|
||||
importlib.reload(cfg)
|
||||
|
||||
def test_tool_not_dict_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||
"""When [tool] is not a dict, defaults are used."""
|
||||
(tmp_path / "pyproject.toml").write_text('tool = "not a dict"\n')
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False)
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.TASK_PREFIX == "DEVX"
|
||||
importlib.reload(cfg)
|
||||
|
||||
def test_devx_not_dict_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None:
|
||||
"""When [tool.devx] is not a dict, defaults are used."""
|
||||
(tmp_path / "pyproject.toml").write_text('[tool]\ndevx = "not a dict"\n')
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False)
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.TASK_PREFIX == "DEVX"
|
||||
importlib.reload(cfg)
|
||||
|
||||
|
||||
class TestTaskPrefixOverride:
|
||||
def test_task_prefix_from_env(self, monkeypatch: object) -> None:
|
||||
"""Verify TASK_PREFIX reads from DEVX_TASK_PREFIX env var."""
|
||||
monkeypatch.setenv("DEVX_TASK_PREFIX", "INFRA")
|
||||
import importlib
|
||||
|
||||
import devx.config as cfg
|
||||
|
||||
importlib.reload(cfg)
|
||||
assert cfg.TASK_PREFIX == "INFRA"
|
||||
assert cfg.TASK_ID_RE.search("INFRA-42")
|
||||
assert not cfg.TASK_ID_RE.search("DEVX-42")
|
||||
# Restore
|
||||
monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False)
|
||||
importlib.reload(cfg)
|
||||
|
||||
Reference in New Issue
Block a user