mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-10 00:16:39 +00:00
fix: runtime config update ignored by config file
Runtime settings were handed back to pydantic-settings as init settings,
which rank below the config file and the environment. Any key already
present in EOS.config.json or in the environment silently discarded the
update, so a bulk PUT /v1/config returned 200 without applying anything,
while the granular PUT /v1/config/{path} endpoint kept working.
Add a dedicated runtime settings source ranked directly below the command
line arguments and record granular updates there as well, so both
endpoints share one store that survives re-evaluation of the settings
sources. Environment variables keep precedence over the config file for
all keys that were not set at runtime.
Also repairs revert_settings() and update(), which passed their data
through the same init settings.
Closes #1303
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
@@ -547,3 +548,133 @@ def test_merge_settings_empty(config_eos):
|
||||
config_eos.merge_settings_from_dict({}) # No changes
|
||||
|
||||
assert config_eos.general.latitude == original_latitude # Should remain unchanged
|
||||
|
||||
|
||||
# ------------------------------------
|
||||
# Runtime settings priority (issue #1303)
|
||||
# ------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_eos_file(config_eos_factory) -> ConfigEOS:
|
||||
"""ConfigEOS with the EOS configuration file as an active settings source."""
|
||||
return config_eos_factory(
|
||||
init={
|
||||
"with_init_settings": True,
|
||||
"with_env_settings": True,
|
||||
"with_dotenv_settings": False,
|
||||
"with_file_settings": True,
|
||||
"with_file_secret_settings": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def write_config_file(config_eos: ConfigEOS, settings: dict[str, Any]) -> None:
|
||||
"""Write settings to the EOS configuration file and load them."""
|
||||
settings = {"general": {"version": config_eos.general.version}, **settings}
|
||||
config_file_path = config_eos.general.config_file_path
|
||||
assert config_file_path is not None
|
||||
config_file_path.write_text(json.dumps(settings), encoding="utf-8")
|
||||
config_eos.reset_settings()
|
||||
|
||||
|
||||
def test_merge_settings_overrides_config_file(config_eos_file):
|
||||
"""Runtime settings take precedence over the EOS configuration file."""
|
||||
write_config_file(
|
||||
config_eos_file,
|
||||
{
|
||||
"optimization": {"genetic": {"individuals": 200}},
|
||||
"pvforecast": {
|
||||
"planes": [
|
||||
{"surface_tilt": 30.0, "surface_azimuth": azimuth, "peakpower": 5.0}
|
||||
for azimuth in (0.0, 90.0, 180.0, 270.0)
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
assert config_eos_file.optimization.genetic.individuals == 200
|
||||
assert len(config_eos_file.pvforecast.planes) == 4
|
||||
|
||||
config_eos_file.merge_settings_from_dict(
|
||||
{
|
||||
"optimization": {"genetic": {"individuals": 300}},
|
||||
"pvforecast": {
|
||||
"planes": [{"surface_tilt": 30.0, "surface_azimuth": 180.0, "peakpower": 5.0}]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert config_eos_file.optimization.genetic.individuals == 300
|
||||
assert len(config_eos_file.pvforecast.planes) == 1
|
||||
|
||||
|
||||
def test_merge_settings_overrides_env(config_eos_file, monkeypatch):
|
||||
"""Runtime settings take precedence over environment variables."""
|
||||
monkeypatch.setenv("EOS_OPTIMIZATION__GENETIC__INDIVIDUALS", "150")
|
||||
config_eos_file.reset_settings()
|
||||
assert config_eos_file.optimization.genetic.individuals == 150
|
||||
|
||||
config_eos_file.merge_settings_from_dict({"optimization": {"genetic": {"individuals": 300}}})
|
||||
|
||||
assert config_eos_file.optimization.genetic.individuals == 300
|
||||
|
||||
|
||||
def test_env_overrides_config_file_after_merge(config_eos_file, monkeypatch):
|
||||
"""Environment variables keep precedence over the config file for untouched keys."""
|
||||
write_config_file(config_eos_file, {"server": {"port": 9000}})
|
||||
monkeypatch.setenv("EOS_SERVER__PORT", "9500")
|
||||
config_eos_file.reset_settings()
|
||||
assert config_eos_file.server.port == 9500
|
||||
|
||||
# A runtime update of an unrelated key must not freeze the env value
|
||||
config_eos_file.merge_settings_from_dict({"general": {"latitude": 51.1657}})
|
||||
assert config_eos_file.general.latitude == 51.1657
|
||||
assert config_eos_file.server.port == 9500
|
||||
|
||||
monkeypatch.setenv("EOS_SERVER__PORT", "9600")
|
||||
config_eos_file.reset_settings()
|
||||
assert config_eos_file.server.port == 9600
|
||||
|
||||
|
||||
def test_reset_settings_drops_runtime_settings(config_eos_file):
|
||||
"""Reset drops runtime settings and falls back to the config file."""
|
||||
write_config_file(config_eos_file, {"optimization": {"genetic": {"individuals": 200}}})
|
||||
|
||||
config_eos_file.merge_settings_from_dict({"optimization": {"genetic": {"individuals": 300}}})
|
||||
assert config_eos_file.optimization.genetic.individuals == 300
|
||||
|
||||
config_eos_file.reset_settings()
|
||||
assert config_eos_file.optimization.genetic.individuals == 200
|
||||
|
||||
|
||||
def test_set_nested_value_survives_merge(config_eos_file):
|
||||
"""Granular updates are not lost by a later bulk update."""
|
||||
write_config_file(config_eos_file, {"optimization": {"genetic": {"individuals": 200}}})
|
||||
|
||||
config_eos_file.set_nested_value("optimization/genetic/individuals", 400)
|
||||
assert config_eos_file.optimization.genetic.individuals == 400
|
||||
|
||||
config_eos_file.merge_settings_from_dict({"general": {"latitude": 51.1657}})
|
||||
assert config_eos_file.optimization.genetic.individuals == 400
|
||||
|
||||
|
||||
def test_revert_settings_restores_backup(config_eos_file):
|
||||
"""Revert restores the backup values even if the config file differs."""
|
||||
write_config_file(config_eos_file, {"optimization": {"genetic": {"individuals": 200}}})
|
||||
|
||||
config_file_path = config_eos_file.general.config_file_path
|
||||
assert config_file_path is not None
|
||||
backup_path = config_file_path.with_suffix(".backup")
|
||||
backup_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"general": {"version": config_eos_file.general.version},
|
||||
"optimization": {"genetic": {"individuals": 500}},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config_eos_file.revert_settings("backup")
|
||||
|
||||
assert config_eos_file.optimization.genetic.individuals == 500
|
||||
|
||||
Reference in New Issue
Block a user