fix(config): report computed fields as read-only on update (#1454)

PUT /v1/config/{path} answered "does not exist in model" when the
target was a computed field such as eos_device_instruction_entity_ids.
Raise an explicit read-only error instead, so API clients can tell
read-only fields from unknown ones.

Closes #913
This commit is contained in:
YTM-21
2026-10-26 07:16:16 +01:00
committed by GitHub
parent 58b968562e
commit 8f4fa6fe94
4 changed files with 35 additions and 1 deletions
+3
View File
@@ -106,6 +106,9 @@ compatibility changes and a short test procedure. These changes are not yet a ta
- GENETIC reads its horizon, interval and prediction settings once at the start of a run. A - GENETIC reads its horizon, interval and prediction settings once at the start of a run. A
configuration change during a run (for example a client adapting the control horizon) no configuration change during a run (for example a client adapting the control horizon) no
longer fails the run with an out-of-bounds error; it applies to the next run. longer fails the run with an out-of-bounds error; it applies to the next run.
- Updating a read-only computed configuration field, e.g. `adapter.homeassistant.eos_solution_entity_ids`
via `PUT /v1/config/{path}`, now reports a read-only error instead of claiming the field does
not exist ([#913](https://github.com/Akkudoktor-EOS/EOS/issues/913)).
## 0.3.0 (2026-03-17) ## 0.3.0 (2026-03-17)
+5
View File
@@ -569,6 +569,11 @@ class PydanticModelNestedValueMixin:
raise TypeError(f"Model '{model}' is not of class type.") raise TypeError(f"Model '{model}' is not of class type.")
if key not in model.model_fields: if key not in model.model_fields:
if key in model.model_computed_fields:
raise TypeError(
f"Field '{key}' of model '{model.__name__}' is a read-only computed "
"field and cannot be updated."
)
raise TypeError(f"Field '{key}' does not exist in model '{model.__name__}'.") raise TypeError(f"Field '{key}' does not exist in model '{model.__name__}'.")
field_annotation = model.model_fields[key].annotation field_annotation = model.model_fields[key].annotation
+12
View File
@@ -486,6 +486,18 @@ def test_set_nested_key(path, value, expected, exception, config_eos):
f"Expected exception {exception}, but got {type(e)}: {e}" f"Expected exception {exception}, but got {type(e)}: {e}"
) )
def test_set_nested_key_read_only_computed_field(config_eos):
"""Setting a computed field is reported as read-only, not as missing."""
path = "adapter/homeassistant/eos_device_instruction_entity_ids"
with pytest.raises(TypeError, match="read-only computed field"):
config_eos.set_nested_value(path, ["sensor.eos_battery1"])
def test_set_nested_key_unknown_field_still_reports_missing(config_eos):
"""A field that does not exist keeps the original 'does not exist' message."""
with pytest.raises(TypeError, match="does not exist in model"):
config_eos.set_nested_value("adapter/homeassistant/no_such_field", 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path, expected_value, exception", "path, expected_value, exception",
+15 -1
View File
@@ -3,7 +3,7 @@ from typing import Any, Optional
import pandas as pd import pandas as pd
import pendulum import pendulum
import pytest import pytest
from pydantic import Field, ValidationError from pydantic import BaseModel, Field, ValidationError, computed_field
from akkudoktoreos.core.pydantic import ( from akkudoktoreos.core.pydantic import (
PydanticBaseModel, PydanticBaseModel,
@@ -199,6 +199,20 @@ class TestPydanticModelNestedValueMixin:
with pytest.raises(TypeError): with pytest.raises(TypeError):
PydanticModelNestedValueMixin._get_key_types(User, "unknown_field") PydanticModelNestedValueMixin._get_key_types(User, "unknown_field")
def test_get_key_types_for_computed_field(self):
"""Test _get_key_types reports a computed field as read-only."""
class WithComputed(BaseModel):
value: int = 1
@computed_field # type: ignore[prop-decorator]
@property
def doubled(self) -> int:
return self.value * 2
with pytest.raises(TypeError, match="read-only computed field"):
PydanticModelNestedValueMixin._get_key_types(WithComputed, "doubled")
def test_get_key_types_for_instance_raises(self, user_instance): def test_get_key_types_for_instance_raises(self, user_instance):
"""Test _get_key_types raises an error for an instance.""" """Test _get_key_types raises an error for an instance."""
with pytest.raises(TypeError): with pytest.raises(TypeError):