2323from typing import Optional
2424from typing import Text # noqa: F401
2525from typing import Tuple # noqa: F401
26+ from typing import TYPE_CHECKING
2627from typing import Union
2728
2829import warnings
3233from launch .frontend import Entity
3334from launch .frontend import expose_action
3435from launch .frontend import Parser
36+ from launch .frontend .type_utils import get_data_type_from_identifier
37+
3538from launch .launch_context import LaunchContext
3639import launch .logging
3740from launch .some_substitutions_type import SomeSubstitutionsType
3841from launch .substitutions import LocalSubstitution
39- from launch .substitutions import TextSubstitution
4042from launch .utilities import ensure_argument_type
4143from launch .utilities import normalize_to_list_of_substitutions
4244from launch .utilities import perform_substitutions
5759
5860import yaml
5961
62+ if TYPE_CHECKING :
63+ from ..descriptions import Parameter
64+
6065
6166@expose_action ('node' )
6267class 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 )
0 commit comments