From 4feb18baeff61468e4e50e81d4560b339b389a5f Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Mon, 26 Jan 2026 17:04:25 +0100 Subject: [PATCH 01/13] add assignment methods in python components --- .../modulo_components/component_interface.py | 70 +++++++++++++++++++ .../test/python/test_component_interface.py | 31 +++++++- source/modulo_core/modulo_core/exceptions.py | 8 +++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index 594a700f..0f1cce58 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -10,6 +10,7 @@ import modulo_core.translators.message_writers as modulo_writers import state_representation as sr from geometry_msgs.msg import TransformStamped +from modulo_interfaces.msg import Assignment as AssignmentMsg from modulo_interfaces.msg import Predicate as PredicateMsg from modulo_interfaces.msg import PredicateCollection from modulo_interfaces.srv import EmptyTrigger, StringTrigger @@ -46,6 +47,7 @@ def __init__(self, node_name: str, *args, **kwargs): super().__init__(node_name, *args, **node_kwargs) self.__step_lock = Lock() self.__parameter_dict: Dict[str, Union[str, sr.Parameter]] = {} + self.__assignment_dict: Dict[str, sr.Parameter] = {} self.__read_only_parameters: Dict[str, bool] = {} self.__pre_set_parameters_callback_called = False self.__set_parameters_result = SetParametersResult() @@ -68,6 +70,9 @@ def __init__(self, node_name: str, *args, **kwargs): self.add_parameter(sr.Parameter("rate", 10.0, sr.ParameterType.DOUBLE), "The rate in Hertz for all periodic callbacks") + self.__assignment_publisher = self.create_publisher(AssignmentMsg, "/assignments", self.__qos) + self.__assignment_message = AssignmentMsg() + self.__assignment_message.node = self.get_fully_qualified_name() self.__predicate_publisher = self.create_publisher(PredicateCollection, "/predicates", self.__qos) self.__predicate_message = PredicateCollection() self.__predicate_message.node = self.get_fully_qualified_name() @@ -342,6 +347,71 @@ def add_predicate(self, name: str, predicate: Union[bool, Callable[[], bool]]) - except Exception as e: self.get_logger().error(f"Failed to add predicate '{name}': {e}") + def add_assignment(self, name: str, type: sr.ParameterType) -> None: + """ + Add an assignment to the dictionary of assignments. + + :param name: The name of the assignment + :param type: The type of the assignment + """ + parsed_name = parse_topic_name(name) + if not parsed_name: + self.get_logger().error((f"The parsed assignment name for '{name}' is empty. Provide a " + "string with valid characters for the assignment name ([a-z0-9_]).")) + return + if parsed_name != name: + self.get_logger().error((f"The parsed assignment name for '{name}' is '{parsed_name}'. Use that " + "to refer to the assignment.")) + if parsed_name in self.__assignment_dict.keys(): + self.get_logger().warn(f"Assignment with name '{parsed_name}' already exists, overwriting.") + else: + self.get_logger().debug(f"Adding assignment '{parsed_name}'.") + try: + self.__assignment_dict[parsed_name] = sr.Parameter(parsed_name, type) + except Exception as e: + self.get_logger().error(f"Failed to add assignment '{parsed_name}': {e}") + + def get_assignment(self, name: str) -> T: + """ + Get the assignment value from the assignment dictionary by its name. + + :param name: The name of the assignment + :raises ParameterError: if the assignment does not exist + :return: The value of the assignment, if the assignment exists and has been assigned + """ + if name not in self.__assignment_dict.keys(): + raise ParameterError(f"Assignment '{name}' is not in the dict of assignments") + try: + return self.__assignment_dict[name].get_value() + except AttributeError as e: + raise ParameterError(f"{e}") + + def set_assignment(self, name: str, value: T) -> None: + """ + Set the value of an assignment. The assignment must have been previously declared. + + :param name: The name of the assignment + :param value: The value of the assignment + """ + if name not in self.__assignment_dict.keys(): + self.get_logger().error( + f"Failed to set assignment '{name}': Assignment does not exist.", throttle_duration_sec=1.0) + return + try: + assigned_parameter = write_parameter( + sr.Parameter(name, value, self.__assignment_dict[name].get_parameter_type())) + self.__assignment_dict[name] = read_parameter_const(assigned_parameter, self.__assignment_dict[name]) + except Exception as e: + self.get_logger().error( + f"Failed to set assignment '{name}': {e}", + throttle_duration_sec=1.0, + ) + raise AssignmentException(f"Failed to set assignment '{name}': {e}") + + message = copy.copy(self.__assignment_message) + message.assignment = assigned_parameter.to_parameter_msg() + self.__assignment_publisher.publish(message) +self.__predicate_message.type = PredicateCollection.COMPONENT def get_predicate(self, name: str) -> bool: """ Get the value of the predicate given as parameter. If the predicate is not found or the callable function fails, diff --git a/source/modulo_components/test/python/test_component_interface.py b/source/modulo_components/test/python/test_component_interface.py index d1fba9ae..155cb7f4 100644 --- a/source/modulo_components/test/python/test_component_interface.py +++ b/source/modulo_components/test/python/test_component_interface.py @@ -7,7 +7,7 @@ import state_representation as sr from modulo_interfaces.srv import EmptyTrigger, StringTrigger from modulo_components.component_interface import ComponentInterface -from modulo_core.exceptions import CoreError, LookupTransformError +from modulo_core.exceptions import CoreError, LookupTransformError, ParameterError, AssignmentException from rclpy.qos import QoSProfile from std_msgs.msg import Bool, String from sensor_msgs.msg import JointState @@ -71,6 +71,35 @@ def test_set_predicate(component_interface): assert not component_interface.get_predicate('bar') +def test_add_assignment(component_interface): + component_interface.add_assignment('string_assignment', sr.ParameterType.STRING) + assert 'string_assignment' in component_interface._ComponentInterface__assignment_dict.keys() + assert component_interface._ComponentInterface__assignment_dict['string_assignment'].is_empty() + # adding an empty assignment should fail + component_interface.add_assignment('', sr.ParameterType.STRING) + assert len(component_interface._ComponentInterface__assignment_dict) == 1 + + +def test_set_get_assignment(component_interface): + # setting a non defined assignment should fail + component_interface.set_assignment('string_assignment', 'test') + with pytest.raises(ParameterError): + component_interface.get_assignment('string_assignment') + component_interface.add_assignment('string_assignment', sr.ParameterType.STRING) + + # setting the wrong type of value should fail + with pytest.raises(AssignmentException): + component_interface.set_assignment('string_assignment', 5) + assert component_interface._ComponentInterface__assignment_dict['string_assignment'].is_empty() + # setting the right type of value should succeed + component_interface.set_assignment('string_assignment', 'test') + assert component_interface.get_assignment('string_assignment') == 'test' + # setting the worng type again and getting should still work + with pytest.raises(AssignmentException): + component_interface.set_assignment('string_assignment', 5) + assert component_interface.get_assignment('string_assignment') == 'test' + + def test_declare_signal(component_interface): component_interface.declare_input("input", "test") assert component_interface.get_parameter_value("input_topic") == "test" diff --git a/source/modulo_core/modulo_core/exceptions.py b/source/modulo_core/modulo_core/exceptions.py index 08cada96..7597fbea 100644 --- a/source/modulo_core/modulo_core/exceptions.py +++ b/source/modulo_core/modulo_core/exceptions.py @@ -74,3 +74,11 @@ class LookupJointPositionsException(CoreError): def __init__(self, message: str): super().__init__(message, "LookupJointPositionsException") + +class AssignmentException(CoreError): + """ + An exception class to notify an error while setting assignments. + """ + + def __init__(self, message: str): + super().__init__(message, "AssignmentException") From b0681e25427dd58355fe4ec3ade393da370c3ee0 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Mon, 26 Jan 2026 17:08:29 +0100 Subject: [PATCH 02/13] fix: typo --- .../modulo_components/modulo_components/component_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index 0f1cce58..5a3c46d4 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -411,7 +411,7 @@ def set_assignment(self, name: str, value: T) -> None: message = copy.copy(self.__assignment_message) message.assignment = assigned_parameter.to_parameter_msg() self.__assignment_publisher.publish(message) -self.__predicate_message.type = PredicateCollection.COMPONENT + def get_predicate(self, name: str) -> bool: """ Get the value of the predicate given as parameter. If the predicate is not found or the callable function fails, From a7ac56dcbba573bc58ccedcbe86c506ba2a3d74b Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Wed, 4 Feb 2026 12:42:10 +0100 Subject: [PATCH 03/13] fix: simplify assignment setting --- .../modulo_components/component_interface.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index 5a3c46d4..2852f239 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -398,9 +398,8 @@ def set_assignment(self, name: str, value: T) -> None: f"Failed to set assignment '{name}': Assignment does not exist.", throttle_duration_sec=1.0) return try: - assigned_parameter = write_parameter( - sr.Parameter(name, value, self.__assignment_dict[name].get_parameter_type())) - self.__assignment_dict[name] = read_parameter_const(assigned_parameter, self.__assignment_dict[name]) + self.__assignment_dict[name].set_value(value) + ros_param = write_parameter(self.__assignment_dict[name]) except Exception as e: self.get_logger().error( f"Failed to set assignment '{name}': {e}", @@ -409,7 +408,7 @@ def set_assignment(self, name: str, value: T) -> None: raise AssignmentException(f"Failed to set assignment '{name}': {e}") message = copy.copy(self.__assignment_message) - message.assignment = assigned_parameter.to_parameter_msg() + message.assignment = ros_param.to_parameter_msg() self.__assignment_publisher.publish(message) def get_predicate(self, name: str) -> bool: From d9aac0f040f088d712bd44e583569caa876d9a83 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Wed, 4 Feb 2026 14:45:10 +0100 Subject: [PATCH 04/13] fix: update tests --- .../test/python/test_component_interface.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/source/modulo_components/test/python/test_component_interface.py b/source/modulo_components/test/python/test_component_interface.py index 155cb7f4..6ebabce2 100644 --- a/source/modulo_components/test/python/test_component_interface.py +++ b/source/modulo_components/test/python/test_component_interface.py @@ -80,24 +80,22 @@ def test_add_assignment(component_interface): assert len(component_interface._ComponentInterface__assignment_dict) == 1 -def test_set_get_assignment(component_interface): - # setting a non defined assignment should fail - component_interface.set_assignment('string_assignment', 'test') +def test_get_set_assignment(component_interface): + component_interface.add_assignment('int_assignment', sr.ParameterType.INT) + with pytest.raises(ParameterError): component_interface.get_assignment('string_assignment') - component_interface.add_assignment('string_assignment', sr.ParameterType.STRING) + component_interface.set_assignment('string_assignment', 'test') + # TODO: Will not throw, pending release of #261 PR in control libraries + with pytest.raises(sr.exceptions.EmptyStateError): + component_interface.get_assignment('int_assignment') # setting the wrong type of value should fail with pytest.raises(AssignmentException): - component_interface.set_assignment('string_assignment', 5) - assert component_interface._ComponentInterface__assignment_dict['string_assignment'].is_empty() - # setting the right type of value should succeed - component_interface.set_assignment('string_assignment', 'test') - assert component_interface.get_assignment('string_assignment') == 'test' - # setting the worng type again and getting should still work - with pytest.raises(AssignmentException): - component_interface.set_assignment('string_assignment', 5) - assert component_interface.get_assignment('string_assignment') == 'test' + component_interface.set_assignment('int_assignment', 'test') + assert component_interface._ComponentInterface__assignment_dict['int_assignment'].is_empty() + component_interface.set_assignment('int_assignment', 5) + assert component_interface.get_assignment('int_assignment') == 5 def test_declare_signal(component_interface): From a80c3773991c100e0c9091ea8bf3069ba961e58b Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Wed, 4 Feb 2026 16:10:38 +0100 Subject: [PATCH 05/13] fix: apply suggestions from review Co-authored-by: Dominic Reber <71256590+domire8@users.noreply.github.com> --- .../modulo_components/modulo_components/component_interface.py | 2 +- source/modulo_core/modulo_core/exceptions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index 2852f239..34aa57ea 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -384,7 +384,7 @@ def get_assignment(self, name: str) -> T: try: return self.__assignment_dict[name].get_value() except AttributeError as e: - raise ParameterError(f"{e}") + raise InvalidAssignmentError(f"{e}") def set_assignment(self, name: str, value: T) -> None: """ diff --git a/source/modulo_core/modulo_core/exceptions.py b/source/modulo_core/modulo_core/exceptions.py index 7597fbea..fa2ddb5d 100644 --- a/source/modulo_core/modulo_core/exceptions.py +++ b/source/modulo_core/modulo_core/exceptions.py @@ -75,7 +75,7 @@ class LookupJointPositionsException(CoreError): def __init__(self, message: str): super().__init__(message, "LookupJointPositionsException") -class AssignmentException(CoreError): +class InvalidAssignmentError(CoreError): """ An exception class to notify an error while setting assignments. """ From 2403c05f3305f07571c35598f1f0503deb437523 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Wed, 4 Feb 2026 16:52:43 +0100 Subject: [PATCH 06/13] fix: tests and exceptions --- .../modulo_components/component_interface.py | 6 +++--- .../test/python/test_component_interface.py | 19 ++++++++++++++----- source/modulo_core/modulo_core/exceptions.py | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index 34aa57ea..dae2bd70 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -376,11 +376,11 @@ def get_assignment(self, name: str) -> T: Get the assignment value from the assignment dictionary by its name. :param name: The name of the assignment - :raises ParameterError: if the assignment does not exist + :raises InvalidAssignmentError: if the assignment does not exist :return: The value of the assignment, if the assignment exists and has been assigned """ if name not in self.__assignment_dict.keys(): - raise ParameterError(f"Assignment '{name}' is not in the dict of assignments") + raise InvalidAssignmentError(f"Assignment '{name}' is not in the dict of assignments") try: return self.__assignment_dict[name].get_value() except AttributeError as e: @@ -405,7 +405,7 @@ def set_assignment(self, name: str, value: T) -> None: f"Failed to set assignment '{name}': {e}", throttle_duration_sec=1.0, ) - raise AssignmentException(f"Failed to set assignment '{name}': {e}") + raise InvalidAssignmentError(f"Failed to set assignment '{name}': {e}") message = copy.copy(self.__assignment_message) message.assignment = ros_param.to_parameter_msg() diff --git a/source/modulo_components/test/python/test_component_interface.py b/source/modulo_components/test/python/test_component_interface.py index 6ebabce2..cd0df196 100644 --- a/source/modulo_components/test/python/test_component_interface.py +++ b/source/modulo_components/test/python/test_component_interface.py @@ -7,7 +7,7 @@ import state_representation as sr from modulo_interfaces.srv import EmptyTrigger, StringTrigger from modulo_components.component_interface import ComponentInterface -from modulo_core.exceptions import CoreError, LookupTransformError, ParameterError, AssignmentException +from modulo_core.exceptions import CoreError, LookupTransformError, InvalidAssignmentError from rclpy.qos import QoSProfile from std_msgs.msg import Bool, String from sensor_msgs.msg import JointState @@ -73,17 +73,26 @@ def test_set_predicate(component_interface): def test_add_assignment(component_interface): component_interface.add_assignment('string_assignment', sr.ParameterType.STRING) - assert 'string_assignment' in component_interface._ComponentInterface__assignment_dict.keys() - assert component_interface._ComponentInterface__assignment_dict['string_assignment'].is_empty() + assert len(component_interface._ComponentInterface__assignment_dict) == 1 # adding an empty assignment should fail component_interface.add_assignment('', sr.ParameterType.STRING) assert len(component_interface._ComponentInterface__assignment_dict) == 1 + # adding the assignment again should just overwrite + component_interface.add_assignment('string_assignment', sr.ParameterType.STRING) + assert len(component_interface._ComponentInterface__assignment_dict) == 1 + # names should be cleaned up + component_interface.add_assignment('7cleEaGn_AaSssiGNgn#ment', sr.ParameterType.STRING) + assert 'clean_assignment' in component_interface._ComponentInterface__assignment_dict.keys() + assert len(component_interface._ComponentInterface__assignment_dict) == 2 + # names without valid characters should fail + component_interface.add_assignment('@@@@@@@', sr.ParameterType.STRING) + assert len(component_interface._ComponentInterface__assignment_dict) == 2 def test_get_set_assignment(component_interface): component_interface.add_assignment('int_assignment', sr.ParameterType.INT) - with pytest.raises(ParameterError): + with pytest.raises(InvalidAssignmentError): component_interface.get_assignment('string_assignment') component_interface.set_assignment('string_assignment', 'test') @@ -91,7 +100,7 @@ def test_get_set_assignment(component_interface): with pytest.raises(sr.exceptions.EmptyStateError): component_interface.get_assignment('int_assignment') # setting the wrong type of value should fail - with pytest.raises(AssignmentException): + with pytest.raises(InvalidAssignmentError): component_interface.set_assignment('int_assignment', 'test') assert component_interface._ComponentInterface__assignment_dict['int_assignment'].is_empty() component_interface.set_assignment('int_assignment', 5) diff --git a/source/modulo_core/modulo_core/exceptions.py b/source/modulo_core/modulo_core/exceptions.py index fa2ddb5d..c901c710 100644 --- a/source/modulo_core/modulo_core/exceptions.py +++ b/source/modulo_core/modulo_core/exceptions.py @@ -77,8 +77,8 @@ def __init__(self, message: str): class InvalidAssignmentError(CoreError): """ - An exception class to notify an error while setting assignments. + An exception class to notify errors when getting the value of an assignment. """ def __init__(self, message: str): - super().__init__(message, "AssignmentException") + super().__init__(message, "InvalidAssignmentError") From 76f419b48ed31b460f4e2f7348ee742955d64ae6 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Thu, 5 Feb 2026 12:06:11 +0100 Subject: [PATCH 07/13] fix: apply suggestions from code review Co-authored-by: Dominic Reber <71256590+domire8@users.noreply.github.com> --- .../modulo_components/component_interface.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index dae2bd70..2e5cc8a3 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -356,12 +356,12 @@ def add_assignment(self, name: str, type: sr.ParameterType) -> None: """ parsed_name = parse_topic_name(name) if not parsed_name: - self.get_logger().error((f"The parsed assignment name for '{name}' is empty. Provide a " + self.get_logger().error((f"The parsed name for assignment '{name}' is empty. Provide a " "string with valid characters for the assignment name ([a-z0-9_]).")) return if parsed_name != name: - self.get_logger().error((f"The parsed assignment name for '{name}' is '{parsed_name}'. Use that " - "to refer to the assignment.")) + self.get_logger().error((f"The parsed name for assignment '{name}' is '{parsed_name}'. Use the parsed name " + "to refer to this assignment.")) if parsed_name in self.__assignment_dict.keys(): self.get_logger().warn(f"Assignment with name '{parsed_name}' already exists, overwriting.") else: @@ -381,10 +381,10 @@ def get_assignment(self, name: str) -> T: """ if name not in self.__assignment_dict.keys(): raise InvalidAssignmentError(f"Assignment '{name}' is not in the dict of assignments") - try: - return self.__assignment_dict[name].get_value() - except AttributeError as e: - raise InvalidAssignmentError(f"{e}") + if self.__assignment_dict[name].is_empty(): + # TODO: remove after control libraries v9.3.1 + raise sr.exceptions.EmptyStateError(f"Parameter '{name}' is empty") + return self.__assignment_dict[name].get_value() def set_assignment(self, name: str, value: T) -> None: """ From 99c07511f119595e8ebaee481296cb77e8bc96ed Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Thu, 5 Feb 2026 12:09:43 +0100 Subject: [PATCH 08/13] fix: update method description --- .../modulo_components/modulo_components/component_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index 2e5cc8a3..7f54159d 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -377,6 +377,7 @@ def get_assignment(self, name: str) -> T: :param name: The name of the assignment :raises InvalidAssignmentError: if the assignment does not exist + :raises EmptyStateError: if the assignment has not been set yet :return: The value of the assignment, if the assignment exists and has been assigned """ if name not in self.__assignment_dict.keys(): From 68216ebf7d314014b2e86768f58ae81d1c3f1ab8 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Thu, 5 Feb 2026 12:11:12 +0100 Subject: [PATCH 09/13] fix: remove exception thrown on set_assignment --- .../modulo_components/modulo_components/component_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index 7f54159d..b62913f0 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -406,7 +406,7 @@ def set_assignment(self, name: str, value: T) -> None: f"Failed to set assignment '{name}': {e}", throttle_duration_sec=1.0, ) - raise InvalidAssignmentError(f"Failed to set assignment '{name}': {e}") + return message = copy.copy(self.__assignment_message) message.assignment = ros_param.to_parameter_msg() From cd678ca93e4ba2f72579c4a7c751a85c498fe432 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Thu, 5 Feb 2026 12:12:23 +0100 Subject: [PATCH 10/13] fix: update tests --- .../modulo_components/test/python/test_component_interface.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/modulo_components/test/python/test_component_interface.py b/source/modulo_components/test/python/test_component_interface.py index cd0df196..66926130 100644 --- a/source/modulo_components/test/python/test_component_interface.py +++ b/source/modulo_components/test/python/test_component_interface.py @@ -91,12 +91,9 @@ def test_add_assignment(component_interface): def test_get_set_assignment(component_interface): component_interface.add_assignment('int_assignment', sr.ParameterType.INT) - with pytest.raises(InvalidAssignmentError): component_interface.get_assignment('string_assignment') component_interface.set_assignment('string_assignment', 'test') - - # TODO: Will not throw, pending release of #261 PR in control libraries with pytest.raises(sr.exceptions.EmptyStateError): component_interface.get_assignment('int_assignment') # setting the wrong type of value should fail From b2e2b15f44a59b752143993120438a78f64c7656 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Thu, 5 Feb 2026 13:17:04 +0100 Subject: [PATCH 11/13] fix: remove expected exception in tests --- .../modulo_components/test/python/test_component_interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/modulo_components/test/python/test_component_interface.py b/source/modulo_components/test/python/test_component_interface.py index 66926130..94b555cb 100644 --- a/source/modulo_components/test/python/test_component_interface.py +++ b/source/modulo_components/test/python/test_component_interface.py @@ -97,8 +97,7 @@ def test_get_set_assignment(component_interface): with pytest.raises(sr.exceptions.EmptyStateError): component_interface.get_assignment('int_assignment') # setting the wrong type of value should fail - with pytest.raises(InvalidAssignmentError): - component_interface.set_assignment('int_assignment', 'test') + component_interface.set_assignment('int_assignment', 'test') assert component_interface._ComponentInterface__assignment_dict['int_assignment'].is_empty() component_interface.set_assignment('int_assignment', 5) assert component_interface.get_assignment('int_assignment') == 5 From 9fd9959c83fa450f5a3d67d99fc44ae4ceeabd7a Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Thu, 5 Feb 2026 13:23:26 +0100 Subject: [PATCH 12/13] fix: apply suggestions from code review Co-authored-by: Dominic Reber <71256590+domire8@users.noreply.github.com> --- .../modulo_components/component_interface.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index b62913f0..a6a6b5cd 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -384,7 +384,7 @@ def get_assignment(self, name: str) -> T: raise InvalidAssignmentError(f"Assignment '{name}' is not in the dict of assignments") if self.__assignment_dict[name].is_empty(): # TODO: remove after control libraries v9.3.1 - raise sr.exceptions.EmptyStateError(f"Parameter '{name}' is empty") + raise sr.exceptions.EmptyStateError(f"{name} state is empty") return self.__assignment_dict[name].get_value() def set_assignment(self, name: str, value: T) -> None: @@ -402,10 +402,7 @@ def set_assignment(self, name: str, value: T) -> None: self.__assignment_dict[name].set_value(value) ros_param = write_parameter(self.__assignment_dict[name]) except Exception as e: - self.get_logger().error( - f"Failed to set assignment '{name}': {e}", - throttle_duration_sec=1.0, - ) + self.get_logger().error(f"Failed to set assignment '{name}': {e}", throttle_duration_sec=1.0) return message = copy.copy(self.__assignment_message) From 7202b0a3cea920ca1c63a90f974130ea66b14d14 Mon Sep 17 00:00:00 2001 From: Spyros Garyfallidis Date: Thu, 5 Feb 2026 13:28:01 +0100 Subject: [PATCH 13/13] fix: formatting --- .../modulo_components/component_interface.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/source/modulo_components/modulo_components/component_interface.py b/source/modulo_components/modulo_components/component_interface.py index a6a6b5cd..030fe93e 100644 --- a/source/modulo_components/modulo_components/component_interface.py +++ b/source/modulo_components/modulo_components/component_interface.py @@ -356,12 +356,14 @@ def add_assignment(self, name: str, type: sr.ParameterType) -> None: """ parsed_name = parse_topic_name(name) if not parsed_name: - self.get_logger().error((f"The parsed name for assignment '{name}' is empty. Provide a " - "string with valid characters for the assignment name ([a-z0-9_]).")) + self.get_logger().error( + f"The parsed name for assignment '{name}' is empty. Provide a " + "string with valid characters for the assignment name ([a-z0-9_]).") return if parsed_name != name: - self.get_logger().error((f"The parsed name for assignment '{name}' is '{parsed_name}'. Use the parsed name " - "to refer to this assignment.")) + self.get_logger().error( + f"The parsed name for assignment '{name}' is '{parsed_name}'. Use the parsed name " + "to refer to this assignment.") if parsed_name in self.__assignment_dict.keys(): self.get_logger().warn(f"Assignment with name '{parsed_name}' already exists, overwriting.") else: