Skip to content

Commit 91097d1

Browse files
authored
Substitutions in parameter files (#168)
Signed-off-by: Ivan Santiago Paunovic <[email protected]>
1 parent 8c05ebb commit 91097d1

File tree

7 files changed

+268
-17
lines changed

7 files changed

+268
-17
lines changed

launch_ros/launch_ros/actions/node.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
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
2726
from typing import Union
2827

2928
import warnings
@@ -59,8 +58,8 @@
5958

6059
import yaml
6160

62-
if TYPE_CHECKING:
63-
from ..descriptions import Parameter
61+
from ..descriptions import Parameter
62+
from ..descriptions import ParameterFile
6463

6564

6665
@expose_action('node')
@@ -257,16 +256,25 @@ def get_nested_dictionary_from_nested_key_value_pairs(params):
257256
normalized_params = []
258257
for param in params:
259258
from_attr = param.get_attr('from', optional=True)
259+
allow_substs = param.get_attr('allow_substs', data_type=bool, optional=True)
260260
name = param.get_attr('name', optional=True)
261261
if from_attr is not None and name is not None:
262262
raise RuntimeError('name and from attributes are mutually exclusive')
263263
elif from_attr is not None:
264264
# 'from' attribute ignores 'name' attribute,
265265
# it's not accepted to be nested,
266266
# and it can not have children.
267-
normalized_params.append(parser.parse_substitution(from_attr))
267+
if isinstance(allow_substs, str):
268+
allow_substs = parser.parse_substitution(allow_substs)
269+
else:
270+
allow_substs = bool(allow_substs)
271+
normalized_params.append(
272+
ParameterFile(parser.parse_substitution(from_attr), allow_substs=allow_substs))
268273
continue
269274
elif name is not None:
275+
if allow_substs is not None:
276+
raise RuntimeError(
277+
"'allow_substs' can only be used together with 'from' attribute")
270278
normalized_params.append(
271279
get_nested_dictionary_from_nested_key_value_pairs([param]))
272280
continue

launch_ros/launch_ros/descriptions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
from .composable_node import ComposableNode
1818
from ..parameter_descriptions import Parameter
19+
from ..parameter_descriptions import ParameterFile
1920
from ..parameter_descriptions import ParameterValue
2021

2122

2223
__all__ = [
2324
'ComposableNode',
2425
'Parameter',
26+
'ParameterFile',
2527
'ParameterValue',
2628
]

launch_ros/launch_ros/parameter_descriptions.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
"""Module for a description of a Parameter."""
1616

17+
import os
18+
from pathlib import Path
19+
from tempfile import NamedTemporaryFile
1720
from typing import List
1821
from typing import Optional
1922
from typing import Text
@@ -24,14 +27,19 @@
2427
from launch import LaunchContext
2528
from launch import SomeSubstitutionsType
2629
from launch import SomeSubstitutionsType_types_tuple
30+
from launch.frontend.parse_substitution import parse_substitution
2731
from launch.substitution import Substitution
32+
from launch.substitutions import SubstitutionFailure
2833
from launch.utilities import ensure_argument_type
2934
from launch.utilities import normalize_to_list_of_substitutions
3035
from launch.utilities import perform_substitutions
3136
from launch.utilities.type_utils import AllowedTypesType
3237
from launch.utilities.type_utils import normalize_typed_substitution
3338
from launch.utilities.type_utils import perform_typed_substitution
3439
from launch.utilities.type_utils import SomeValueType
40+
from launch.utilities.typing_file_path import FilePath
41+
42+
import yaml
3543

3644
if TYPE_CHECKING:
3745
from .parameters_type import EvaluatedParameterValue
@@ -154,3 +162,99 @@ def evaluate(self, context: LaunchContext) -> Tuple[Text, 'EvaluatedParameterVal
154162
self.__evaluated_parameter_name = name
155163
self.__evaluated_parameter_rule = (name, value)
156164
return (name, value)
165+
166+
167+
class ParameterFile:
168+
"""Describes a ROS parameter file."""
169+
170+
def __init__(
171+
self,
172+
param_file: Union[FilePath, SomeSubstitutionsType],
173+
*,
174+
allow_substs: [bool, SomeSubstitutionsType] = False
175+
) -> None:
176+
"""
177+
Construct a parameter file description.
178+
179+
:param param_file: Path to a parameter file.
180+
:param allow_subst: Allow substitutions in the parameter file.
181+
"""
182+
ensure_argument_type(
183+
param_file,
184+
SomeSubstitutionsType_types_tuple + (os.PathLike, bytes),
185+
'param_file',
186+
'ParameterFile()'
187+
)
188+
ensure_argument_type(
189+
allow_substs,
190+
bool,
191+
'allow_subst',
192+
'ParameterFile()'
193+
)
194+
self.__param_file: Union[List[Substitution], FilePath] = param_file
195+
if isinstance(param_file, SomeSubstitutionsType_types_tuple):
196+
self.__param_file = normalize_to_list_of_substitutions(param_file)
197+
self.__allow_substs = normalize_typed_substitution(allow_substs, data_type=bool)
198+
self.__evaluated_allow_substs: Optional[bool] = None
199+
self.__evaluated_param_file: Optional[Path] = None
200+
self.__created_tmp_file = False
201+
202+
@property
203+
def param_file(self) -> Union[FilePath, List[Substitution]]:
204+
"""Getter for parameter file."""
205+
if self.__evaluated_param_file is not None:
206+
return self.__evaluated_param_file
207+
return self.__param_file
208+
209+
@property
210+
def allow_substs(self) -> Union[bool, List[Substitution]]:
211+
"""Getter for allow substitutions argument."""
212+
if self.__evaluated_allow_substs is not None:
213+
return self.__evaluated_allow_substs
214+
return self.__allow_substs
215+
216+
def __str__(self) -> Text:
217+
return (
218+
'launch_ros.description.ParameterFile'
219+
f'(param_file={self.param_file}, allow_substs={self.allow_substs})'
220+
)
221+
222+
def evaluate(self, context: LaunchContext) -> Path:
223+
"""Evaluate and return a parameter file path."""
224+
if self.__evaluated_param_file is not None:
225+
return self.__evaluated_param_file
226+
227+
param_file = self.__param_file
228+
if isinstance(param_file, list):
229+
# list of substitutions
230+
param_file = perform_substitutions(context, self.__param_file)
231+
232+
allow_substs = perform_typed_substitution(context, self.__allow_substs, data_type=bool)
233+
param_file_path: Path = Path(param_file)
234+
if allow_substs:
235+
with open(param_file_path, 'r') as f, NamedTemporaryFile(
236+
mode='w', prefix='launch_params_', delete=False
237+
) as h:
238+
parsed = perform_substitutions(context, parse_substitution(f.read()))
239+
try:
240+
yaml.safe_load(parsed)
241+
except Exception:
242+
raise SubstitutionFailure(
243+
'The substituted parameter file is not a valid yaml file')
244+
h.write(parsed)
245+
param_file_path = Path(h.name)
246+
self.__created_tmp_file = True
247+
self.__evaluated_param_file = param_file_path
248+
return param_file_path
249+
250+
def cleanup(self):
251+
"""Delete created temporary files."""
252+
if self.__created_tmp_file and self.__evaluated_param_file is not None:
253+
try:
254+
os.unlink(self.__evaluated_param_file)
255+
except FileNotFoundError:
256+
pass
257+
self.__evaluated_param_file = None
258+
259+
def __del__(self):
260+
self.cleanup()

launch_ros/launch_ros/parameters_type.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from launch.substitution import Substitution
2828

2929
from .parameter_descriptions import Parameter as ParameterDescription
30+
from .parameter_descriptions import ParameterFile
3031
from .parameter_descriptions import ParameterValue as ParameterValueDescription
3132

3233

@@ -36,7 +37,7 @@
3637
_MultiValueType = Union[
3738
Sequence[str], Sequence[int], Sequence[float], Sequence[bool], bytes]
3839

39-
SomeParameterFile = Union[SomeSubstitutionsType, pathlib.Path]
40+
SomeParameterFile = Union[SomeSubstitutionsType, pathlib.Path, ParameterFile]
4041
SomeParameterName = Sequence[Union[Substitution, str]]
4142
SomeParameterValue = Union[
4243
ParameterValueDescription,
@@ -59,7 +60,7 @@
5960
# parameter names and values
6061
SomeParameters = Sequence[Union[SomeParameterFile, ParameterDescription, SomeParametersDict]]
6162

62-
ParameterFile = Sequence[Substitution]
63+
ParameterFile = ParameterFile # re-export
6364
ParameterName = Sequence[Substitution]
6465
ParameterValue = Union[
6566
Sequence[Substitution],

launch_ros/launch_ros/utilities/evaluate_parameters.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import yaml
3333

3434
from ..parameter_descriptions import Parameter as ParameterDescription
35+
from ..parameter_descriptions import ParameterFile
3536
from ..parameter_descriptions import ParameterValue as ParameterValueDescription
3637
from ..parameters_type import EvaluatedParameters
3738
from ..parameters_type import EvaluatedParameterValue
@@ -140,10 +141,9 @@ def evaluate_parameters(context: LaunchContext, parameters: Parameters) -> Evalu
140141
"""
141142
output_params = [] # type: List[Union[pathlib.Path, Dict[str, EvaluatedParameterValue]]]
142143
for param in parameters:
143-
# If it's a list of substitutions then evaluate them to a string and return a pathlib.Path
144-
if isinstance(param, tuple) and len(param) and isinstance(param[0], Substitution):
144+
if isinstance(param, ParameterFile):
145145
# Evaluate a list of Substitution to a file path
146-
output_params.append(pathlib.Path(perform_substitutions(context, list(param))))
146+
output_params.append(param.evaluate(context))
147147
elif isinstance(param, ParameterDescription):
148148
output_params.append(param)
149149
elif isinstance(param, Mapping):

launch_ros/launch_ros/utilities/normalize_parameters.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
from collections.abc import Mapping
1818
from collections.abc import Sequence
19-
import pathlib
2019
from typing import cast
2120
from typing import List
2221
from typing import Optional
@@ -34,8 +33,8 @@
3433
import yaml
3534

3635
from ..parameter_descriptions import Parameter as ParameterDescription
36+
from ..parameter_descriptions import ParameterFile
3737
from ..parameter_descriptions import ParameterValue as ParameterValueDescription
38-
from ..parameters_type import ParameterFile # noqa: F401
3938
from ..parameters_type import Parameters
4039
from ..parameters_type import ParametersDict
4140
from ..parameters_type import ParameterValue
@@ -179,16 +178,15 @@ def normalize_parameters(parameters: SomeParameters) -> Parameters:
179178
if isinstance(parameters, str) or not isinstance(parameters, Sequence):
180179
raise TypeError('Expecting list of parameters, got {}'.format(parameters))
181180

182-
normalized_params = [] # type: List[Union[ParameterFile, ParametersDict]]
181+
normalized_params: List[Union[ParameterFile, ParametersDict, ParameterDescription]] = []
183182
for param in parameters:
184183
if isinstance(param, Mapping):
185184
normalized_params.append(normalize_parameter_dict(param))
186185
elif isinstance(param, ParameterDescription):
187186
normalized_params.append(param)
187+
elif isinstance(param, ParameterFile):
188+
normalized_params.append(param)
188189
else:
189-
# It's a path, normalize to a list of substitutions
190-
if isinstance(param, pathlib.Path):
191-
param = str(param)
192-
ensure_argument_type(param, SomeSubstitutionsType_types_tuple, 'parameters')
193-
normalized_params.append(tuple(normalize_to_list_of_substitutions(param)))
190+
# It's a path
191+
normalized_params.append(ParameterFile(param))
194192
return tuple(normalized_params)

0 commit comments

Comments
 (0)