fix: add __post_init__ validation to Settings dataclass (#65)

Reject invalid configuration at startup: negative timeouts, zero
concurrency, bad table mode. Reports all errors at once.
This commit is contained in:
Pier-Jean Malandrino 2026-04-07 16:59:37 +02:00
parent 24cfd567f2
commit 2c254382c8
2 changed files with 72 additions and 0 deletions

View file

@ -25,6 +25,27 @@ class Settings:
default_factory=lambda: ["http://localhost:3000", "http://localhost:5173"]
)
def __post_init__(self) -> None:
errors: list[str] = []
if self.document_timeout <= 0:
errors.append(f"document_timeout must be > 0 (got {self.document_timeout})")
if self.conversion_timeout <= 0:
errors.append(f"conversion_timeout must be > 0 (got {self.conversion_timeout})")
if self.max_concurrent_analyses < 1:
errors.append(
f"max_concurrent_analyses must be >= 1 (got {self.max_concurrent_analyses})"
)
if self.max_page_count < 0:
errors.append(f"max_page_count must be >= 0 (got {self.max_page_count})")
if self.max_file_size < 0:
errors.append(f"max_file_size must be >= 0 (got {self.max_file_size})")
if self.default_table_mode not in ("accurate", "fast"):
errors.append(
f"default_table_mode must be 'accurate' or 'fast' (got '{self.default_table_mode}')"
)
if errors:
raise ValueError("Invalid settings:\n " + "\n ".join(errors))
@classmethod
def from_env(cls) -> Settings:
"""Build a Settings instance from environment variables."""

View file

@ -29,6 +29,57 @@ class TestSettingsDefaults:
s.upload_dir = "/other" # type: ignore[misc]
class TestSettingsValidation:
def test_negative_document_timeout_rejected(self):
import pytest
with pytest.raises(ValueError, match="document_timeout must be > 0"):
Settings(document_timeout=-1.0)
def test_zero_document_timeout_rejected(self):
import pytest
with pytest.raises(ValueError, match="document_timeout must be > 0"):
Settings(document_timeout=0)
def test_negative_conversion_timeout_rejected(self):
import pytest
with pytest.raises(ValueError, match="conversion_timeout must be > 0"):
Settings(conversion_timeout=-1)
def test_zero_max_concurrent_rejected(self):
import pytest
with pytest.raises(ValueError, match="max_concurrent_analyses must be >= 1"):
Settings(max_concurrent_analyses=0)
def test_negative_max_page_count_rejected(self):
import pytest
with pytest.raises(ValueError, match="max_page_count must be >= 0"):
Settings(max_page_count=-1)
def test_negative_max_file_size_rejected(self):
import pytest
with pytest.raises(ValueError, match="max_file_size must be >= 0"):
Settings(max_file_size=-1)
def test_invalid_table_mode_rejected(self):
import pytest
with pytest.raises(ValueError, match="default_table_mode must be"):
Settings(default_table_mode="turbo")
def test_multiple_errors_reported(self):
import pytest
with pytest.raises(ValueError, match="document_timeout") as exc_info:
Settings(document_timeout=-1, conversion_timeout=-1)
assert "conversion_timeout" in str(exc_info.value)
class TestSettingsFromEnv:
def test_reads_env_vars(self, monkeypatch):
monkeypatch.setenv("APP_VERSION", "1.2.3")