diff --git a/model_api/docs/source/guides/model-configuration.md b/model_api/docs/source/guides/model-configuration.md index 3a84c0330..1664da99c 100644 --- a/model_api/docs/source/guides/model-configuration.md +++ b/model_api/docs/source/guides/model-configuration.md @@ -2,6 +2,27 @@ Model's static method `create_model()` has two overloads. One constructs the model from a string (a path or a model name) and the other takes an already constructed `InferenceAdapter`. The first overload configures a created model with values taken from `configuration` dict function argument and from model's intermediate representation (IR) stored in `.xml` in `model_info` section of `rt_info`. Values provided in `configuration` have priority over values in IR `rt_info`. If no value is specified in `configuration` nor in `rt_info` the default value for a model wrapper is used. For Python configuration values are accessible as model wrapper member fields. +## Runtime parameter modification + +Parameters can be read and modified after model creation using `get_param()` and `set_param()`: + +```python +from model_api.models import Model + +model = Model.create_model("model.xml") + +# Read current value +current_threshold = model.get_param("confidence_threshold") + +# Modify parameter with validation +model.set_param("confidence_threshold", 0.7) + +# Parameters are also accessible via the params descriptor +print(model.params.confidence_threshold) +``` + +`set_param()` validates the value against the parameter definition and raises `WrapperError` if validation fails. Unknown parameter names are logged as warnings and ignored. + ## List of values The list features only model wrappers which introduce new configuration values in their hierarchy. diff --git a/model_api/src/model_api/models/model.py b/model_api/src/model_api/models/model.py index b3e1fd519..25e7f326f 100644 --- a/model_api/src/model_api/models/model.py +++ b/model_api/src/model_api/models/model.py @@ -123,6 +123,29 @@ def get_param(self, name: str) -> Any: return self._parameters_cache[name].default_value return self.raise_error(f"Parameter '{name}' not found") + def set_param(self, name: str, value: Any) -> None: + """Sets a parameter value, validating it against the parameter definition. + + Args: + name (str): parameter name + value (Any): parameter value + """ + parameters = self.parameters() + + if name in parameters: + errors = parameters[name].validate(value) + if errors: + self.logger.error(f'Error with "{name}" parameter:') + for _error in errors: + self.logger.error(f"\t{_error}") + self.raise_error("Incorrect user configuration") + value = parameters[name].get_value(value) + self.__setattr__(f"_{name}", value) + else: + self.logger.warning( + f'The parameter "{name}" not found in {self.__model__} wrapper, will be omitted', + ) + def get_cached_parameters(self) -> dict[str, Any]: """Get cached parameters, initializing cache if needed. @@ -432,19 +455,7 @@ def _load_config(self, config: dict[str, Any]) -> None: for name, value in config.items(): if value is None: continue - if name in parameters: - errors = parameters[name].validate(value) - if errors: - self.logger.error(f'Error with "{name}" parameter:') - for _error in errors: - self.logger.error(f"\t{_error}") - self.raise_error("Incorrect user configuration") - value = parameters[name].get_value(value) - self.__setattr__(f"_{name}", value) - else: - self.logger.warning( - f'The parameter "{name}" not found in {self.__model__} wrapper, will be omitted', - ) + self.set_param(name, value) @classmethod def raise_error(cls, message) -> NoReturn: diff --git a/model_api/tests/unit/models/test_model_core.py b/model_api/tests/unit/models/test_model_core.py index 111261b87..b8852124a 100644 --- a/model_api/tests/unit/models/test_model_core.py +++ b/model_api/tests/unit/models/test_model_core.py @@ -182,6 +182,49 @@ def test_get_param_unknown_raises(self): model.get_param("nonexistent_parameter_xyz") +class TestModelSetParam: + """Tests for set_param.""" + + def test_set_param_valid_value(self): + """Valid param sets underscore-prefixed attribute.""" + from model_api.models.image_model import ImageModel + + adapter = _make_adapter() + model = ImageModel(adapter, configuration={}, preload=False) + model.set_param("resize_type", "fit_to_window") + assert model.params.resize_type == "fit_to_window" + assert model.get_param("resize_type") == "fit_to_window" + + def test_set_param_invalid_value_raises(self): + """Validation failure raises WrapperError.""" + from model_api.models.image_model import ImageModel + + adapter = _make_adapter() + model = ImageModel(adapter, configuration={}, preload=False) + with pytest.raises(WrapperError, match="Incorrect user configuration"): + model.set_param("resize_type", "INVALID_RESIZE_TYPE") + + def test_set_param_unknown_warns(self, caplog): + """Unknown param logs warning and is omitted.""" + adapter = _make_adapter() + model = Model(adapter, configuration={}, preload=False) + with caplog.at_level(logging.WARNING): + model.set_param("unknown_param_xyz", 42) + assert any("unknown_param_xyz" in r.message for r in caplog.records) + assert any("not found" in r.message for r in caplog.records) + assert not hasattr(model, "_unknown_param_xyz") + + def test_set_param_applies_get_value_transformation(self): + """Value is transformed by parameter's get_value method.""" + from model_api.models.image_model import ImageModel + + adapter = _make_adapter() + model = ImageModel(adapter, configuration={}, preload=False) + # resize_type is a StringValue which returns value as-is, but validates options + model.set_param("resize_type", "standard") + assert model.params.resize_type == "standard" + + class TestModelGetCachedParameters: """Tests for get_cached_parameters (lines 124-132)."""