Skip to content

Commit 9250313

Browse files
authored
Fix problems when parsing a Command Substitution as a parameter value (#137)
Signed-off-by: Ivan Santiago Paunovic <[email protected]>
1 parent 1f0ea9f commit 9250313

File tree

10 files changed

+524
-66
lines changed

10 files changed

+524
-66
lines changed

launch_ros/launch_ros/actions/node.py

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing import Optional
2424
from typing import Text # noqa: F401
2525
from typing import Tuple # noqa: F401
26+
from typing import TYPE_CHECKING
2627
from typing import Union
2728

2829
import warnings
@@ -32,11 +33,12 @@
3233
from launch.frontend import Entity
3334
from launch.frontend import expose_action
3435
from launch.frontend import Parser
36+
from launch.frontend.type_utils import get_data_type_from_identifier
37+
3538
from launch.launch_context import LaunchContext
3639
import launch.logging
3740
from launch.some_substitutions_type import SomeSubstitutionsType
3841
from launch.substitutions import LocalSubstitution
39-
from launch.substitutions import TextSubstitution
4042
from launch.utilities import ensure_argument_type
4143
from launch.utilities import normalize_to_list_of_substitutions
4244
from launch.utilities import perform_substitutions
@@ -57,6 +59,9 @@
5759

5860
import yaml
5961

62+
if TYPE_CHECKING:
63+
from ..descriptions import Parameter
64+
6065

6166
@expose_action('node')
6267
class Node(ExecuteProcess):
@@ -159,6 +164,7 @@ def __init__(
159164
"Only use 'executable'"
160165
)
161166
executable = node_executable
167+
162168
if package is not None:
163169
cmd = [ExecutableInPackage(package=package, executable=executable)]
164170
else:
@@ -207,7 +213,7 @@ def __init__(
207213

208214
self.__expanded_node_name = self.UNSPECIFIED_NODE_NAME
209215
self.__expanded_node_namespace = self.UNSPECIFIED_NODE_NAMESPACE
210-
self.__expanded_parameter_files = None # type: Optional[List[Text]]
216+
self.__expanded_parameter_arguments = None # type: Optional[List[Tuple[Text, bool]]]
211217
self.__final_node_name = None # type: Optional[Text]
212218
self.__expanded_remappings = None # type: Optional[List[Tuple[Text, Text]]]
213219

@@ -218,27 +224,28 @@ def __init__(
218224
@staticmethod
219225
def parse_nested_parameters(params, parser):
220226
"""Normalize parameters as expected by Node constructor argument."""
227+
from ..descriptions import ParameterValue
228+
221229
def get_nested_dictionary_from_nested_key_value_pairs(params):
222230
"""Convert nested params in a nested dictionary."""
223231
param_dict = {}
224232
for param in params:
225233
name = tuple(parser.parse_substitution(param.get_attr('name')))
226-
value = param.get_attr('value', data_type=None, optional=True)
234+
type_identifier = param.get_attr('type', data_type=None, optional=True)
235+
data_type = None
236+
if type_identifier is not None:
237+
data_type = get_data_type_from_identifier(type_identifier)
238+
value = param.get_attr('value', data_type=data_type, optional=True)
227239
nested_params = param.get_attr('param', data_type=List[Entity], optional=True)
228240
if value is not None and nested_params:
229-
raise RuntimeError('param and value attributes are mutually exclusive')
241+
raise RuntimeError(
242+
'nested parameters and value attributes are mutually exclusive')
243+
if data_type is not None and nested_params:
244+
raise RuntimeError(
245+
'nested parameters and type attributes are mutually exclusive')
230246
elif value is not None:
231-
def normalize_scalar_value(value):
232-
if isinstance(value, str):
233-
value = parser.parse_substitution(value)
234-
if len(value) == 1 and isinstance(value[0], TextSubstitution):
235-
value = value[0].text # python `str` are not converted like yaml
236-
return value
237-
if isinstance(value, list):
238-
value = [normalize_scalar_value(x) for x in value]
239-
else:
240-
value = normalize_scalar_value(value)
241-
param_dict[name] = value
247+
some_value = parser.parse_if_substitutions(value)
248+
param_dict[name] = ParameterValue(some_value, value_type=data_type)
242249
elif nested_params:
243250
param_dict.update({
244251
name: get_nested_dictionary_from_nested_key_value_pairs(nested_params)
@@ -325,7 +332,13 @@ def _create_params_file_from_dict(self, params):
325332
yaml.dump(param_dict, h, default_flow_style=False)
326333
return param_file_path
327334

335+
def _get_parameter_rule(self, param: 'Parameter', context: LaunchContext):
336+
name, value = param.evaluate(context)
337+
return f'{name}:={yaml.dump(value)}'
338+
328339
def _perform_substitutions(self, context: LaunchContext) -> None:
340+
# Here to avoid cyclic import
341+
from ..descriptions import Parameter
329342
try:
330343
if self.__substitutions_performed:
331344
# This function may have already been called by a subclass' `execute`, for example.
@@ -365,31 +378,36 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
365378
# so they can be overriden with specific parameters of this Node
366379
global_params = context.launch_configurations.get('ros_params', None)
367380
if global_params is not None or self.__parameters is not None:
368-
self.__expanded_parameter_files = []
381+
self.__expanded_parameter_arguments = []
369382
if global_params is not None:
370383
param_file_path = self._create_params_file_from_dict(global_params)
371-
self.__expanded_parameter_files.append(param_file_path)
384+
self.__expanded_parameter_arguments.append((param_file_path, True))
372385
cmd_extension = ['--params-file', f'{param_file_path}']
373386
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
374387
assert os.path.isfile(param_file_path)
375388
# expand parameters too
376389
if self.__parameters is not None:
377390
evaluated_parameters = evaluate_parameters(context, self.__parameters)
378-
for i, params in enumerate(evaluated_parameters):
391+
for params in evaluated_parameters:
392+
is_file = False
379393
if isinstance(params, dict):
380-
param_file_path = self._create_params_file_from_dict(params)
381-
assert os.path.isfile(param_file_path)
394+
param_argument = self._create_params_file_from_dict(params)
395+
is_file = True
396+
assert os.path.isfile(param_argument)
382397
elif isinstance(params, pathlib.Path):
383-
param_file_path = str(params)
398+
param_argument = str(params)
399+
is_file = True
400+
elif isinstance(params, Parameter):
401+
param_argument = self._get_parameter_rule(params, context)
384402
else:
385403
raise RuntimeError('invalid normalized parameters {}'.format(repr(params)))
386-
if not os.path.isfile(param_file_path):
404+
if is_file and not os.path.isfile(param_argument):
387405
self.__logger.warning(
388-
'Parameter file path is not a file: {}'.format(param_file_path),
406+
'Parameter file path is not a file: {}'.format(param_argument),
389407
)
390408
continue
391-
self.__expanded_parameter_files.append(param_file_path)
392-
cmd_extension = ['--params-file', f'{param_file_path}']
409+
self.__expanded_parameter_arguments.append((param_argument, is_file))
410+
cmd_extension = ['--params-file' if is_file else '-p', f'{param_argument}']
393411
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
394412
# expand remappings too
395413
global_remaps = context.launch_configurations.get('ros_remaps', None)

launch_ros/launch_ros/descriptions/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
"""descriptions Module."""
1616

1717
from .composable_node import ComposableNode
18+
from ..parameter_descriptions import Parameter
19+
from ..parameter_descriptions import ParameterValue
20+
1821

1922
__all__ = [
2023
'ComposableNode',
24+
'Parameter',
25+
'ParameterValue',
2126
]
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# Copyright 2020 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Module for a description of a Parameter."""
16+
17+
from typing import List
18+
from typing import Optional
19+
from typing import Text
20+
from typing import Tuple
21+
from typing import TYPE_CHECKING
22+
from typing import Union
23+
24+
from launch import LaunchContext
25+
from launch import SomeSubstitutionsType
26+
from launch import SomeSubstitutionsType_types_tuple
27+
from launch.substitution import Substitution
28+
from launch.utilities import ensure_argument_type
29+
from launch.utilities import normalize_to_list_of_substitutions
30+
from launch.utilities import perform_substitutions
31+
from launch.utilities.type_utils import AllowedTypesType
32+
from launch.utilities.type_utils import normalize_typed_substitution
33+
from launch.utilities.type_utils import perform_typed_substitution
34+
from launch.utilities.type_utils import SomeValueType
35+
36+
if TYPE_CHECKING:
37+
from .parameters_type import EvaluatedParameterValue
38+
39+
40+
class ParameterValue:
41+
"""Describes a ROS parameter value."""
42+
43+
def __init__(
44+
self,
45+
value: SomeValueType,
46+
*,
47+
value_type: Optional[AllowedTypesType] = None
48+
) -> None:
49+
"""
50+
Construct a parameter value description.
51+
52+
:param value: Value or substitution that can be resolved to a value.
53+
:param value_type: Used when `value` is a substitution, to coerce the result.
54+
Can be one of:
55+
- Scalar types: int, bool, float, str
56+
- List types: List[int], List[bool], List[float], List[str]
57+
- None: will use yaml rules, and check that the result matches one of the above.
58+
"""
59+
self.__value = normalize_typed_substitution(value, value_type)
60+
self.__value_type = value_type
61+
self.__evaluated_parameter_value: Optional['EvaluatedParameterValue'] = None
62+
63+
@property
64+
def value(self) -> SomeValueType:
65+
"""Getter for parameter value."""
66+
if self.__evaluated_parameter_value is not None:
67+
return self.__evaluated_parameter_value
68+
return self.__value
69+
70+
@property
71+
def value_type(self) -> AllowedTypesType:
72+
"""Getter for parameter value type."""
73+
return self.__value_type
74+
75+
def __str__(self) -> Text:
76+
return (
77+
'launch_ros.description.ParameterValue'
78+
f'(value={self.value}, value_type={self.value_type})'
79+
)
80+
81+
def evaluate(self, context: LaunchContext) -> 'EvaluatedParameterValue':
82+
"""Evaluate and return parameter rule."""
83+
self.__evaluated_parameter_value = perform_typed_substitution(
84+
context, self.value, self.value_type)
85+
return self.__evaluated_parameter_value
86+
87+
88+
class Parameter:
89+
"""Describes a ROS parameter."""
90+
91+
def __init__(
92+
self,
93+
name: SomeSubstitutionsType,
94+
value: SomeValueType,
95+
*,
96+
value_type: Optional[AllowedTypesType] = None
97+
) -> None:
98+
"""
99+
Construct a parameter description.
100+
101+
:param name: Name of the parameter.
102+
:param value: Value of the parameter.
103+
:param value_type: Used when `value` is a substitution, to coerce the result.
104+
Can be one of:
105+
- A scalar type: `int`, `str`, `float`, `bool`.
106+
`bool` are written like in `yaml`.
107+
Both `1` and `1.` are valid floats.
108+
- An uniform list: `List[int]`, `List[str]`, `List[float]`, `List[bool]`.
109+
Lists are written like in `yaml`.
110+
- `None`, which means that yaml rules will be used.
111+
The result of the convertion must be one of the above types,
112+
if not `ValueError` is raised.
113+
If value is not a substitution and this parameter is provided,
114+
it will be used to check `value` type.
115+
"""
116+
ensure_argument_type(name, SomeSubstitutionsType_types_tuple, 'name')
117+
118+
self.__name = normalize_to_list_of_substitutions(name)
119+
self.__parameter_value = ParameterValue(value, value_type=value_type)
120+
self.__evaluated_parameter_name: Optional[Text] = None
121+
self.__evaluated_parameter_rule: Optional[Tuple[Text, 'EvaluatedParameterValue']] = None
122+
123+
@property
124+
def name(self) -> Union[List[Substitution], Text]:
125+
"""Getter for parameter name."""
126+
if self.__evaluated_parameter_name is not None:
127+
return self.__evaluated_parameter_name
128+
return self.__name
129+
130+
@property
131+
def value(self) -> SomeValueType:
132+
"""Getter for parameter value."""
133+
return self.__parameter_value.value
134+
135+
@property
136+
def value_type(self) -> AllowedTypesType:
137+
"""Getter for parameter value type."""
138+
return self.__parameter_value.value_type
139+
140+
def __str__(self) -> Text:
141+
return (
142+
'launch_ros.description.Parameter'
143+
f'(name={self.name}, value={self.value}, value_type={self.value_type})'
144+
)
145+
146+
def evaluate(self, context: LaunchContext) -> Tuple[Text, 'EvaluatedParameterValue']:
147+
"""Evaluate and return parameter rule."""
148+
if self.__evaluated_parameter_rule is not None:
149+
return self.__evaluated_parameter_rule
150+
151+
name = perform_substitutions(context, self.name)
152+
value = self.__parameter_value.evaluate(context)
153+
154+
self.__evaluated_parameter_name = name
155+
self.__evaluated_parameter_rule = (name, value)
156+
return (name, value)

launch_ros/launch_ros/parameters_type.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,42 +23,59 @@
2323
from typing import Union
2424

2525
from launch.some_substitutions_type import SomeSubstitutionsType
26+
from launch.some_substitutions_type import SomeSubstitutionsType_types_tuple
2627
from launch.substitution import Substitution
2728

29+
from .parameter_descriptions import Parameter as ParameterDescription
30+
from .parameter_descriptions import ParameterValue as ParameterValueDescription
31+
2832

2933
# Parameter value types used below
34+
_SingleValueType_types_tuple = (str, int, float, bool)
3035
_SingleValueType = Union[str, int, float, bool]
3136
_MultiValueType = Union[
3237
Sequence[str], Sequence[int], Sequence[float], Sequence[bool], bytes]
3338

3439
SomeParameterFile = Union[SomeSubstitutionsType, pathlib.Path]
3540
SomeParameterName = Sequence[Union[Substitution, str]]
36-
SomeParameterValue = Union[SomeSubstitutionsType,
37-
Sequence[SomeSubstitutionsType],
38-
_SingleValueType,
39-
_MultiValueType]
41+
SomeParameterValue = Union[
42+
ParameterValueDescription,
43+
SomeSubstitutionsType,
44+
Sequence[SomeSubstitutionsType],
45+
_SingleValueType,
46+
_MultiValueType
47+
]
48+
SomeParameterValue_types_tuple = (
49+
SomeSubstitutionsType_types_tuple +
50+
_SingleValueType_types_tuple +
51+
(bytes,)
52+
)
4053

4154
# TODO(sloretz) Recursive type when mypy supports them python/mypy#731
4255
_SomeParametersDict = Mapping[SomeParameterName, Any]
4356
SomeParametersDict = Mapping[SomeParameterName, Union[SomeParameterValue, _SomeParametersDict]]
4457

4558
# Paths to yaml files with parameters, or dictionaries of parameters, or pairs of
4659
# parameter names and values
47-
SomeParameters = Sequence[Union[SomeParameterFile, Mapping[SomeParameterName, SomeParameterValue]]]
60+
SomeParameters = Sequence[Union[SomeParameterFile, ParameterDescription, SomeParametersDict]]
4861

4962
ParameterFile = Sequence[Substitution]
5063
ParameterName = Sequence[Substitution]
51-
ParameterValue = Union[Sequence[Substitution],
52-
Sequence[Sequence[Substitution]],
53-
_SingleValueType,
54-
_MultiValueType]
64+
ParameterValue = Union[
65+
Sequence[Substitution],
66+
Sequence[Sequence[Substitution]],
67+
_SingleValueType,
68+
_MultiValueType,
69+
ParameterValueDescription]
5570

5671
# Normalized (flattened to avoid having a recursive type) parameter dict
5772
ParametersDict = Dict[ParameterName, ParameterValue]
5873

5974
# Normalized parameters
60-
Parameters = Sequence[Union[ParameterFile, ParametersDict]]
75+
Parameters = Sequence[Union[ParameterFile, ParametersDict, ParameterDescription]]
6176

6277
EvaluatedParameterValue = Union[_SingleValueType, _MultiValueType]
6378
# Evaluated parameters: filenames or dictionary after substitutions have been evaluated
64-
EvaluatedParameters = Sequence[Union[pathlib.Path, Dict[str, EvaluatedParameterValue]]]
79+
EvaluatedParameters = Sequence[
80+
Union[pathlib.Path, ParameterDescription, Dict[str, EvaluatedParameterValue]]
81+
]

0 commit comments

Comments
 (0)