diff --git a/modelon/impact/client/entities/custom_function.py b/modelon/impact/client/entities/custom_function.py index c860bd8c..5e542ca5 100644 --- a/modelon/impact/client/entities/custom_function.py +++ b/modelon/impact/client/entities/custom_function.py @@ -26,8 +26,17 @@ logger = logging.getLogger(__name__) -class _Parameter: - __slots__ = ["_name", "_value", "_value_type", "_valid_values"] +class CustomFunctionParameter: + """A single analysis parameter of a custom function.""" + + __slots__ = [ + "_name", + "_value", + "_value_type", + "_valid_values", + "_display_name", + "_description", + ] _JSON_2_PY_TYPE = { "Number": ( @@ -43,16 +52,46 @@ class _Parameter: "VariableNames": (list,), } - def __init__(self, name: str, value: Any, value_type: str, valid_values: List[Any]): + def __init__( + self, + name: str, + value: Any, + value_type: str, + valid_values: List[Any], + display_name: str = "", + description: str = "", + ): self._name = name self._value = value self._value_type = value_type self._valid_values = valid_values + self._display_name = display_name + self._description = description @property def name(self) -> str: return self._name + @property + def display_name(self) -> str: + """Human-readable name for the parameter, as shown in Modelon Impact. + + Differs from 'name' in ways that cannot be derived from it: the 'dynamic' custom + function's stop time is named 'final_time' and displayed as 'Stop Time'. Falls + back to 'name' when the signature declares no display name. + + """ + return self._display_name or self._name + + @property + def description(self) -> str: + """Description of what the parameter controls. + + Empty when not declared. + + """ + return self._description + @property def type(self) -> str: return self._value_type @@ -113,11 +152,13 @@ def __init__( self._workspace_id = workspace_id self._parameter_data = parameter_data self._param_by_name = { - p["name"]: _Parameter( + p["name"]: CustomFunctionParameter( p["name"], p.get("defaultValue", ""), p["type"], p.get("values", []), + p.get("displayName", ""), + p.get("description", ""), ) for p in parameter_data } @@ -177,6 +218,24 @@ def with_parameters(self, **modified: Any) -> CustomFunction: return new + def get_parameters(self) -> List[CustomFunctionParameter]: + """Returns the custom function's parameters. + + Use this over 'parameter_values' when the parameters are to be shown to + someone: it carries each parameter's display name and description, which + 'parameter_values' does not. + + Returns: + A list of CustomFunctionParameter class objects. + + Example:: + + for parameter in custom_function.get_parameters(): + print(parameter.display_name, parameter.value) + + """ + return list(self._param_by_name.values()) + @property def parameter_values(self) -> ParameterDict: """Custom_function parameters and value as a dictionary.""" diff --git a/tests/impact/client/entities/test_custom_function.py b/tests/impact/client/entities/test_custom_function.py index 5cdec071..f2e73920 100644 --- a/tests/impact/client/entities/test_custom_function.py +++ b/tests/impact/client/entities/test_custom_function.py @@ -11,6 +11,7 @@ IDs, create_case_entity, create_custom_artifact_uri_entity, + create_custom_function_entity, create_experiment_entity, create_workspace_entity, ) @@ -22,6 +23,46 @@ def _get_dynamic_cf(self, client_helper): dynamic = workspace.get_custom_function("dynamic") return dynamic + def test_parameters_expose_the_signatures_display_name(self): + """The display name is what Impact shows, and it is not derivable. + + 'final_time' is displayed as 'Stop Time', so a caller presenting parameters to + someone has to read it from the signature rather than reformat the name. + + """ + custom_function = create_custom_function_entity( + IDs.WORKSPACE_ID_PRIMARY, + IDs.DYNAMIC_CF, + [ + { + "name": "final_time", + "type": "Number", + "defaultValue": 1, + "displayName": "Stop Time", + "description": "The stop time of the simulation", + } + ], + ) + + (parameter,) = custom_function.get_parameters() + + assert parameter.name == "final_time" + assert parameter.display_name == "Stop Time" + assert parameter.description == "The stop time of the simulation" + assert parameter.value == 1 + + def test_parameter_display_name_falls_back_to_the_name(self): + custom_function = create_custom_function_entity( + IDs.WORKSPACE_ID_PRIMARY, + IDs.DYNAMIC_CF, + [{"name": "tolerance", "type": "Number", "defaultValue": 1e-6}], + ) + + (parameter,) = custom_function.get_parameters() + + assert parameter.display_name == "tolerance" + assert parameter.description == "" + @pytest.mark.experimental def test_custom_function_with_parameters_ok(self, custom_function): workspace = create_workspace_entity(IDs.WORKSPACE_ID_PRIMARY)