From 8f4fa6fe947a61b67c095806a17b857c587a1fac Mon Sep 17 00:00:00 2001 From: YTM-21 Date: Mon, 26 Oct 2026 03:16:16 -0300 Subject: [PATCH] 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 --- CHANGELOG.md | 3 +++ src/akkudoktoreos/core/pydantic.py | 5 +++++ tests/test_config.py | 12 ++++++++++++ tests/test_pydantic.py | 16 +++++++++++++++- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d46a7a6..cb3c93e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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. +- 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) diff --git a/src/akkudoktoreos/core/pydantic.py b/src/akkudoktoreos/core/pydantic.py index 6fe7afd0..ac30100a 100644 --- a/src/akkudoktoreos/core/pydantic.py +++ b/src/akkudoktoreos/core/pydantic.py @@ -569,6 +569,11 @@ class PydanticModelNestedValueMixin: raise TypeError(f"Model '{model}' is not of class type.") 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__}'.") field_annotation = model.model_fields[key].annotation diff --git a/tests/test_config.py b/tests/test_config.py index f230ed96..a96c9c8b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -486,6 +486,18 @@ def test_set_nested_key(path, value, expected, exception, config_eos): 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( "path, expected_value, exception", diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py index cb18b235..d7781814 100644 --- a/tests/test_pydantic.py +++ b/tests/test_pydantic.py @@ -3,7 +3,7 @@ from typing import Any, Optional import pandas as pd import pendulum import pytest -from pydantic import Field, ValidationError +from pydantic import BaseModel, Field, ValidationError, computed_field from akkudoktoreos.core.pydantic import ( PydanticBaseModel, @@ -199,6 +199,20 @@ class TestPydanticModelNestedValueMixin: with pytest.raises(TypeError): 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): """Test _get_key_types raises an error for an instance.""" with pytest.raises(TypeError):