diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index 93e262688..0c611ecbf 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -113,16 +113,32 @@ def get_attr( `launch_yaml` does not apply type coercion, it only checks if the read value is of the correct type. """ - if name not in self.__element: + attribute_name = name + if ( + self.type_name == 'arg' and + name == 'choice' and + name not in self.__element and + 'choices' in self.__element + ): + attribute_name = 'choices' + if attribute_name not in self.__element: if not optional: raise AttributeError( "Can not find attribute '{}' in Entity '{}'".format( name, self.type_name)) else: return None - self.__read_keys.add(name) - data = self.__element[name] + self.__read_keys.add(attribute_name) + data = self.__element[attribute_name] if check_is_list_entity(data_type): + if attribute_name != name: + if is_instance_of(data, List[str], can_be_str=False): + return [Entity({'value': value}, name) for value in data] + raise TypeError( + "Attribute '{}' of Entity '{}' expected to be a list of strings.".format( + attribute_name, self.type_name + ) + ) if isinstance(data, list) and isinstance(data[0], dict): return [Entity(child, name) for child in data] raise TypeError( diff --git a/launch_yaml/test/launch_yaml/test_arg.py b/launch_yaml/test/launch_yaml/test_arg.py new file mode 100644 index 000000000..3c39c30bb --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_arg.py @@ -0,0 +1,93 @@ +# Copyright 2026 Old-Ding +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test parsing a launch argument.""" + +import io +import textwrap + +from launch.actions import DeclareLaunchArgument + +from parser_no_extensions import load_no_extensions + +import pytest + + +def test_arg_choices(): + yaml_file = textwrap.dedent( + """ + launch: + - arg: + name: ur_type + default: ur5e + choices: [ur3, ur3e, ur5e] + """ + ) + root_entity, parser = load_no_extensions(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + argument = launch_description.entities[0] + assert isinstance(argument, DeclareLaunchArgument) + assert argument.choices == ['ur3', 'ur3e', 'ur5e'] + + +def test_arg_legacy_choices(): + yaml_file = textwrap.dedent( + """ + launch: + - arg: + name: ur_type + choice: + - value: ur3 + - value: ur3e + """ + ) + root_entity, parser = load_no_extensions(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + argument = launch_description.entities[0] + assert isinstance(argument, DeclareLaunchArgument) + assert argument.choices == ['ur3', 'ur3e'] + + +def test_arg_choices_reject_non_string_values(): + yaml_file = textwrap.dedent( + """ + launch: + - arg: + name: ur_type + choices: [ur3, 5] + """ + ) + root_entity, parser = load_no_extensions(io.StringIO(yaml_file)) + + with pytest.raises(TypeError, match="'choices'.*list of strings"): + parser.parse_description(root_entity) + + +def test_arg_choices_reject_legacy_and_shorthand_together(): + yaml_file = textwrap.dedent( + """ + launch: + - arg: + name: ur_type + choice: + - value: ur3 + choices: [ur3e] + """ + ) + root_entity, parser = load_no_extensions(io.StringIO(yaml_file)) + + with pytest.raises(ValueError, match="Unexpected key.*'choices'"): + parser.parse_description(root_entity)