Skip to content

Commit 1bb1483

Browse files
olivermeyerclaude
andcommitted
feat(database): resolve env_file from active context in DatabaseSettings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e6bd18d commit 1bb1483

3 files changed

Lines changed: 79 additions & 5 deletions

File tree

src/aignostics_foundry_core/database.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import multiprocessing.util
1616
import urllib.parse
1717
from collections.abc import AsyncGenerator
18+
from pathlib import Path
1819
from typing import Any
1920

2021
from loguru import logger
@@ -50,20 +51,45 @@ class DatabaseSettings(OpaqueSettings):
5051
pool_timeout: float = 30.0
5152
name: str | None = None
5253

53-
def __init__(self, _env_prefix: str | None = None, **kwargs: Any) -> None: # noqa: ANN401
54-
"""Initialise settings, deriving env prefix from the active FoundryContext when not given.
54+
def __init__(
55+
self,
56+
_env_prefix: str | None = None,
57+
_env_file: list[Path] | None = None,
58+
**kwargs: Any, # noqa: ANN401
59+
) -> None:
60+
"""Initialise settings, deriving env prefix and env files from the active FoundryContext when not given.
5561
5662
Args:
5763
_env_prefix: Optional explicit environment variable prefix (e.g. ``"MYAPP_DB_"``).
5864
When ``None``, the prefix is derived from the active FoundryContext as
5965
``f"{get_context().env_prefix}DB_"``.
66+
_env_file: Optional explicit list of ``.env`` files to read settings from.
67+
When ``None`` and a :class:`~aignostics_foundry_core.foundry.FoundryContext` is
68+
active, the context's :attr:`~aignostics_foundry_core.foundry.FoundryContext.env_file`
69+
list is used so that all settings sources remain consistent. When ``None`` and no
70+
context is available (but ``_env_prefix`` is provided explicitly), env-file loading
71+
is skipped.
6072
**kwargs: Forwarded to :class:`~pydantic_settings.BaseSettings`.
73+
74+
Raises:
75+
RuntimeError: If both ``_env_prefix`` is absent and no active context is installed.
6176
"""
62-
if _env_prefix is None:
77+
if _env_prefix is None or _env_file is None:
6378
from aignostics_foundry_core.foundry import get_context # noqa: PLC0415
6479

65-
_env_prefix = f"{get_context().env_prefix}DB_"
66-
super().__init__(_env_prefix=_env_prefix, **kwargs) # pyright: ignore[reportCallIssue]
80+
try:
81+
ctx = get_context()
82+
if _env_prefix is None:
83+
_env_prefix = f"{ctx.env_prefix}DB_"
84+
if _env_file is None:
85+
_env_file = ctx.env_file
86+
except RuntimeError:
87+
if _env_prefix is None:
88+
raise
89+
if _env_file is not None:
90+
super().__init__(_env_prefix=_env_prefix, _env_file=_env_file, **kwargs) # pyright: ignore[reportCallIssue]
91+
else:
92+
super().__init__(_env_prefix=_env_prefix, **kwargs) # pyright: ignore[reportCallIssue]
6793

6894
def get_url(self) -> str:
6995
"""Return the database URL string, optionally substituting the database name.

tests/aignostics_foundry_core/database_settings_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for DatabaseSettings."""
22

33
from collections.abc import Generator
4+
from pathlib import Path
45

56
import pytest
67

@@ -11,6 +12,9 @@
1112
# Constants (SonarQube S1192)
1213
POSTGRES_URL = "postgresql+asyncpg://user:pass@localhost:5432/postgres"
1314
SQLITE_URL = "sqlite+aiosqlite:///test.db"
15+
WRONG_SQLITE_URL = "sqlite+aiosqlite:///wrong.db"
16+
MYAPP_ENV_PREFIX = "MYAPP_"
17+
MYAPP_DB_URL_KEY = "MYAPP_DB_URL"
1418
CUSTOM_PREFIX = "CUSTOM_DB_"
1519
CUSTOM_PREFIX_URL_ENV = "CUSTOM_DB_URL"
1620
DEFAULT_POOL_SIZE = 10
@@ -141,3 +145,44 @@ def test_url_is_masked_in_repr() -> None:
141145
representation = repr(settings)
142146
assert "pass" not in representation
143147
assert "**" in representation or "SecretStr" in representation
148+
149+
150+
# ---------------------------------------------------------------------------
151+
# env-file resolution via context (integration)
152+
# ---------------------------------------------------------------------------
153+
154+
155+
@pytest.mark.integration
156+
def test_database_settings_reads_url_from_env_file_via_context(tmp_path: Path) -> None:
157+
"""DatabaseSettings() with no args reads URL from context env_file when context is set."""
158+
env_file = tmp_path / ".env"
159+
env_file.write_text(f"{MYAPP_DB_URL_KEY}={SQLITE_URL}\n")
160+
161+
ctx = make_context(env_prefix=MYAPP_ENV_PREFIX, env_file=[env_file])
162+
set_context(ctx)
163+
164+
settings = DatabaseSettings()
165+
assert settings.get_url() == SQLITE_URL
166+
167+
168+
@pytest.mark.integration
169+
def test_database_settings_explicit_env_file_overrides_context(tmp_path: Path) -> None:
170+
"""An explicit _env_file passed to DatabaseSettings() takes precedence over the context env_file."""
171+
context_env_file = tmp_path / "context.env"
172+
context_env_file.write_text(f"{MYAPP_DB_URL_KEY}={WRONG_SQLITE_URL}\n")
173+
174+
explicit_env_file = tmp_path / "explicit.env"
175+
explicit_env_file.write_text(f"{MYAPP_DB_URL_KEY}={SQLITE_URL}\n")
176+
177+
ctx = make_context(env_prefix=MYAPP_ENV_PREFIX, env_file=[context_env_file])
178+
set_context(ctx)
179+
180+
settings = DatabaseSettings(_env_file=[explicit_env_file])
181+
assert settings.get_url() == SQLITE_URL
182+
183+
184+
@pytest.mark.integration
185+
def test_database_settings_no_context_raises_without_prefix() -> None:
186+
"""DatabaseSettings() raises RuntimeError when no context is installed and no prefix is given."""
187+
with pytest.raises(RuntimeError, match="get_context\\(\\) called before set_context"):
188+
DatabaseSettings()

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def make_context( # noqa: PLR0913
7070
project_path: Path | None = None,
7171
repository_url: str = "",
7272
database: DatabaseSettings | None = None,
73+
env_file: list[Path] | None = None,
7374
**kwargs: bool,
7475
) -> FoundryContext:
7576
"""Create a minimal FoundryContext for testing.
@@ -83,6 +84,7 @@ def make_context( # noqa: PLR0913
8384
repository_url: The project repository URL (defaults to ``""``).
8485
database: Optional :class:`~aignostics_foundry_core.database.DatabaseSettings`
8586
instance to attach to the context.
87+
env_file: Optional list of ``.env`` file paths to attach to the context.
8688
**kwargs: Optional boolean flags forwarded to :class:`FoundryContext`
8789
(``is_test``, ``is_cli``, ``is_container``, ``is_library``).
8890
"""
@@ -96,5 +98,6 @@ def make_context( # noqa: PLR0913
9698
project_path=project_path,
9799
repository_url=repository_url,
98100
database=database,
101+
env_file=env_file or [],
99102
**kwargs, # type: ignore[arg-type]
100103
)

0 commit comments

Comments
 (0)