Package roslaunch :: Module loader
[frames] | no frames]

Source Code for Module roslaunch.loader

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2008, Willow Garage, Inc. 
  4  # All rights reserved. 
  5  # 
  6  # Redistribution and use in source and binary forms, with or without 
  7  # modification, are permitted provided that the following conditions 
  8  # are met: 
  9  # 
 10  #  * Redistributions of source code must retain the above copyright 
 11  #    notice, this list of conditions and the following disclaimer. 
 12  #  * Redistributions in binary form must reproduce the above 
 13  #    copyright notice, this list of conditions and the following 
 14  #    disclaimer in the documentation and/or other materials provided 
 15  #    with the distribution. 
 16  #  * Neither the name of Willow Garage, Inc. nor the names of its 
 17  #    contributors may be used to endorse or promote products derived 
 18  #    from this software without specific prior written permission. 
 19  # 
 20  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 21  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 22  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 23  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 24  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 25  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 26  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 27  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 28  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 29  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 30  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 31  # POSSIBILITY OF SUCH DAMAGE. 
 32  # 
 33  # Revision $Id$ 
 34   
 35  """ 
 36  General routines and representations for loading roslaunch model. 
 37  """ 
 38   
 39  import errno 
 40  import os 
 41  from copy import deepcopy 
 42   
 43  import yaml 
 44   
 45  from roslaunch.core import Param, RosbinExecutable, RLException, PHASE_SETUP 
 46   
 47  from rosgraph.names import make_global_ns, ns_join, PRIV_NAME, load_mappings, is_legal_name, canonicalize_name 
 48   
 49  #lazy-import global for yaml and rosparam 
 50  rosparam = None 
 51   
52 -class LoadException(RLException):
53 """Error loading data as specified (e.g. cannot find included files, etc...)""" 54 pass
55 56 #TODO: lists, maps(?)
57 -def convert_value(value, type_):
58 """ 59 Convert a value from a string representation into the specified 60 type 61 62 @param value: string representation of value 63 @type value: str 64 @param type_: int, double, string, bool, or auto 65 @type type_: str 66 @raise ValueError: if parameters are invalid 67 """ 68 type_ = type_.lower() 69 # currently don't support XML-RPC date, dateTime, maps, or list 70 # types 71 if type_ == 'auto': 72 #attempt numeric conversion 73 try: 74 if '.' in value: 75 return float(value) 76 else: 77 return int(value) 78 except ValueError as e: 79 pass 80 #bool 81 lval = value.lower() 82 if lval == 'true' or lval == 'false': 83 return convert_value(value, 'bool') 84 #string 85 return value 86 elif type_ == 'str' or type_ == 'string': 87 return value 88 elif type_ == 'int': 89 return int(value) 90 elif type_ == 'double': 91 return float(value) 92 elif type_ == 'bool' or type_ == 'boolean': 93 value = value.lower().strip() 94 if value == 'true' or value == '1': 95 return True 96 elif value == 'false' or value == '0': 97 return False 98 raise ValueError("%s is not a '%s' type"%(value, type_)) 99 elif type_ == 'yaml': 100 try: 101 return yaml.load(value) 102 except yaml.parser.ParserError as e: 103 raise ValueError(e) 104 else: 105 raise ValueError("Unknown type '%s'"%type_)
106
107 -def process_include_args(context):
108 """ 109 Processes arg declarations in context and makes sure that they are 110 properly declared for passing into an included file. Also will 111 correctly setup the context for passing to the included file. 112 """ 113 114 # make sure all arguments have values. arg_names and resolve_dict 115 # are cleared when the context was initially created. 116 arg_dict = context.include_resolve_dict.get('arg', {}) 117 for arg in context.arg_names: 118 if not arg in arg_dict: 119 raise LoadException("include args must have declared values") 120 121 # save args that were passed so we can check for unused args in post-processing 122 context.args_passed = list(arg_dict.keys()) 123 # clear arg declarations so included file can re-declare 124 context.arg_names = [] 125 126 # swap in new resolve dict for passing 127 context.resolve_dict = context.include_resolve_dict 128 context.include_resolve_dict = None
129
130 -def post_process_include_args(context):
131 bad = [a for a in context.args_passed if a not in context.arg_names] 132 if bad: 133 raise LoadException("unused args [%s] for include of [%s]"%(', '.join(bad), context.filename))
134
135 -def load_sysargs_into_context(context, argv):
136 """ 137 Load in ROS remapping arguments as arg assignments for context. 138 139 @param context: context to load into. context's resolve_dict for 'arg' will be reinitialized with values. 140 @type context: L{LoaderContext{ 141 @param argv: command-line arguments 142 @type argv: [str] 143 """ 144 # we use same command-line spec as ROS nodes 145 mappings = load_mappings(argv) 146 context.resolve_dict['arg'] = mappings
147
148 -class LoaderContext(object):
149 """ 150 Container for storing current loader context (e.g. namespace, 151 local parameter state, remapping state). 152 """ 153
154 - def __init__(self, ns, filename, parent=None, params=None, env_args=None, \ 155 resolve_dict=None, include_resolve_dict=None, arg_names=None):
156 """ 157 @param ns: namespace 158 @type ns: str 159 @param filename: name of file this is being loaded from 160 @type filename: str 161 @param resolve_dict: (optional) resolution dictionary for substitution args 162 @type resolve_dict: dict 163 @param include_resolve_dict: special resolution dictionary for 164 <include> tags. Must be None if this is not an <include> 165 context. 166 @type include_resolve_dict: dict 167 @param arg_names: name of args that have been declared in this context 168 @type arg_names: [str] 169 """ 170 self.parent = parent 171 self.ns = make_global_ns(ns or '/') 172 self._remap_args = [] 173 self.params = params or [] 174 self.env_args = env_args or [] 175 self.filename = filename 176 # for substitution args 177 self.resolve_dict = resolve_dict or {} 178 # arg names. Args that have been declared in this context 179 self.arg_names = arg_names or [] 180 # special scoped resolve dict for processing in <include> tag 181 self.include_resolve_dict = include_resolve_dict or None 182 # when this context was created via include, was pass_all_args set? 183 self.pass_all_args = False
184
185 - def add_param(self, p):
186 """ 187 Add a ~param to the context. ~params are evaluated by any node 188 declarations that occur later in the same context. 189 190 @param p: parameter 191 @type p: L{Param} 192 """ 193 194 # override any params already set 195 matches = [m for m in self.params if m.key == p.key] 196 for m in matches: 197 self.params.remove(m) 198 self.params.append(p)
199
200 - def add_remap(self, remap):
201 """ 202 Add a new remap setting to the context. if a remap already 203 exists with the same from key, it will be removed 204 205 @param remap: remap setting 206 @type remap: (str, str) 207 """ 208 remap = [canonicalize_name(x) for x in remap] 209 if not remap[0] or not remap[1]: 210 raise RLException("remap from/to attributes cannot be empty") 211 if not is_legal_name(remap[0]): 212 raise RLException("remap from [%s] is not a valid ROS name"%remap[0]) 213 if not is_legal_name(remap[1]): 214 raise RLException("remap to [%s] is not a valid ROS name"%remap[1]) 215 216 matches = [r for r in self._remap_args if r[0] == remap[0]] 217 for m in matches: 218 self._remap_args.remove(m) 219 self._remap_args.append(remap)
220
221 - def add_arg(self, name, default=None, value=None, doc=None):
222 """ 223 Add 'arg' to existing context. Args are only valid for their immediate context. 224 """ 225 if name in self.arg_names: 226 # Ignore the duplication if pass_all_args was set 227 if not self.pass_all_args: 228 raise LoadException("arg '%s' has already been declared"%name) 229 else: 230 self.arg_names.append(name) 231 232 resolve_dict = self.resolve_dict if self.include_resolve_dict is None else self.include_resolve_dict 233 234 if not 'arg' in resolve_dict: 235 resolve_dict['arg'] = {} 236 arg_dict = resolve_dict['arg'] 237 238 # args can only be declared once. they must also have one and 239 # only value at the time that they are declared. 240 if value is not None: 241 # value is set, error if declared in our arg dict as args 242 # with set values are constant/grounded. 243 # But don't error if pass_all_args was used to include this 244 # context; rather just override the passed-in value. 245 if name in arg_dict and not self.pass_all_args: 246 raise LoadException("cannot override arg '%s', which has already been set"%name) 247 arg_dict[name] = value 248 elif default is not None: 249 # assign value if not in context 250 if name not in arg_dict: 251 arg_dict[name] = default 252 else: 253 # no value or default: appending to arg_names is all we 254 # need to do as it declares the existence of the arg. 255 pass 256 257 # add arg documentation string dict if it doesn't exist yet and if it can be used 258 if not 'arg_doc' in resolve_dict: 259 resolve_dict['arg_doc'] = {} 260 arg_doc_dict = resolve_dict['arg_doc'] 261 262 if not value: 263 # store the documentation for this argument 264 arg_doc_dict[name] = (doc, default)
265 266
267 - def remap_args(self):
268 """ 269 @return: copy of the current remap arguments 270 @rtype: [(str, str)] 271 """ 272 if self.parent: 273 args = [] 274 # filter out any parent remap args that have the same from key 275 for pr in self.parent.remap_args(): 276 if not [r for r in self._remap_args if r[0] == pr[0]]: 277 args.append(pr) 278 args.extend(self._remap_args) 279 return args 280 return self._remap_args[:]
281
282 - def include_child(self, ns, filename):
283 """ 284 Create child namespace based on include inheritance rules 285 @param ns: sub-namespace of child context, or None if the 286 child context shares the same namespace 287 @type ns: str 288 @param filename: name of include file 289 @type filename: str 290 @return: A child xml context that inherits from this context 291 @rtype: L{LoaderContext}jj 292 """ 293 ctx = self.child(ns) 294 # arg declarations are reset across include boundaries 295 ctx.arg_names = [] 296 ctx.filename = filename 297 # keep the resolve_dict for now, we will do all new assignments into include_resolve_dict 298 ctx.include_resolve_dict = {} 299 #del ctx.resolve_dict['arg'] 300 return ctx
301
302 - def child(self, ns):
303 """ 304 @param ns: sub-namespace of child context, or None if the 305 child context shares the same namespace 306 @type ns: str 307 @return: A child xml context that inherits from this context 308 @rtype: L{LoaderContext} 309 """ 310 if ns: 311 if ns[0] == '/': # global (discouraged) 312 child_ns = ns 313 elif ns == PRIV_NAME: # ~name 314 # private names can only be scoped privately or globally 315 child_ns = PRIV_NAME 316 else: 317 child_ns = ns_join(self.ns, ns) 318 else: 319 child_ns = self.ns 320 return LoaderContext(child_ns, self.filename, parent=self, 321 params=self.params, env_args=self.env_args[:], 322 resolve_dict=deepcopy(self.resolve_dict), 323 arg_names=self.arg_names[:], include_resolve_dict=self.include_resolve_dict)
324 325 #TODO: in-progress refactorization. I'm slowly separating out 326 #non-XML-specific logic from xmlloader and moving into Loader. Soon 327 #this will mean that it will be easier to write coverage tests for 328 #lower-level logic. 329
330 -class Loader(object):
331 """ 332 Lower-level library for loading ROS launch model. It provides an 333 abstraction between the representation (e.g. XML) and the 334 validation of the property values. 335 """ 336
337 - def add_param(self, ros_config, param_name, param_value, verbose=True):
338 """ 339 Add L{Param} instances to launch config. Dictionary values are 340 unrolled into individual parameters. 341 342 @param ros_config: launch configuration 343 @type ros_config: L{ROSLaunchConfig} 344 @param param_name: name of parameter namespace to load values 345 into. If param_name is '/', param_value must be a dictionary 346 @type param_name: str 347 @param param_value: value to assign to param_name. If 348 param_value is a dictionary, it's values will be unrolled 349 into individual parameters. 350 @type param_value: str 351 @raise ValueError: if parameters cannot be processed into valid Params 352 """ 353 354 # shouldn't ever happen 355 if not param_name: 356 raise ValueError("no parameter name specified") 357 358 if param_name == '/' and type(param_value) != dict: 359 raise ValueError("Cannot load non-dictionary types into global namespace '/'") 360 361 if type(param_value) == dict: 362 # unroll params 363 for k, v in param_value.items(): 364 self.add_param(ros_config, ns_join(param_name, k), v, verbose=verbose) 365 else: 366 ros_config.add_param(Param(param_name, param_value), verbose=verbose)
367
368 - def load_rosparam(self, context, ros_config, cmd, param, file_, text, verbose=True, subst_function=None):
369 """ 370 Load rosparam setting 371 372 @param context: Loader context 373 @type context: L{LoaderContext} 374 @param ros_config: launch configuration 375 @type ros_config: L{ROSLaunchConfig} 376 @param cmd: 'load', 'dump', or 'delete' 377 @type cmd: str 378 @param file_: filename for rosparam to use or None 379 @type file_: str 380 @param text: text for rosparam to load. Ignored if file_ is set. 381 @type text: str 382 @raise ValueError: if parameters cannot be processed into valid rosparam setting 383 """ 384 if not cmd in ('load', 'dump', 'delete'): 385 raise ValueError("command must be 'load', 'dump', or 'delete'") 386 if file_ is not None: 387 if cmd == 'load' and not os.path.isfile(file_): 388 raise ValueError("file does not exist [%s]"%file_) 389 if cmd == 'delete': 390 raise ValueError("'file' attribute is invalid with 'delete' command.") 391 392 full_param = ns_join(context.ns, param) if param else context.ns 393 394 if cmd == 'dump': 395 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, file_, full_param), PHASE_SETUP)) 396 elif cmd == 'delete': 397 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, full_param), PHASE_SETUP)) 398 elif cmd == 'load': 399 # load YAML text 400 if file_: 401 with open(file_, 'r') as f: 402 text = f.read() 403 404 if subst_function is not None: 405 text = subst_function(text) 406 # parse YAML text 407 # - lazy import: we have to import rosparam in oder to to configure the YAML constructors 408 global rosparam 409 if rosparam is None: 410 import rosparam 411 try: 412 data = yaml.load(text) 413 # #3162: if there is no YAML, load() will return an 414 # empty string. We want an empty dictionary instead 415 # for our representation of empty. 416 if data is None: 417 data = {} 418 except yaml.MarkedYAMLError as e: 419 if not file_: 420 raise ValueError("Error within YAML block:\n\t%s\n\nYAML is:\n%s"%(str(e), text)) 421 else: 422 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 423 except Exception as e: 424 if not file_: 425 raise ValueError("invalid YAML: %s\n\nYAML is:\n%s"%(str(e), text)) 426 else: 427 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 428 429 # 'param' attribute is required for non-dictionary types 430 if not param and type(data) != dict: 431 raise ValueError("'param' attribute must be set for non-dictionary values") 432 433 self.add_param(ros_config, full_param, data, verbose=verbose) 434 435 else: 436 raise ValueError("unknown command %s"%cmd)
437 438
439 - def load_env(self, context, ros_config, name, value):
440 """ 441 Load environment variable setting 442 443 @param context: Loader context 444 @type context: L{LoaderContext} 445 @param ros_config: launch configuration 446 @type ros_config: L{ROSLaunchConfig} 447 @param name: environment variable name 448 @type name: str 449 @param value: environment variable value 450 @type value: str 451 """ 452 if not name: 453 raise ValueError("'name' attribute must be non-empty") 454 context.env_args.append((name, value))
455 456
457 - def param_value(self, verbose, name, ptype, value, textfile, binfile, command):
458 """ 459 Parse text representation of param spec into Python value 460 @param name: param name, for error message use only 461 @type name: str 462 @param verbose: print verbose output 463 @type verbose: bool 464 @param textfile: name of text file to load from, or None 465 @type textfile: str 466 @param binfile: name of binary file to load from, or None 467 @type binfile: str 468 @param command: command to execute for parameter value, or None 469 @type command: str 470 @raise ValueError: if parameters are invalid 471 """ 472 if value is not None: 473 return convert_value(value.strip(), ptype) 474 elif textfile is not None: 475 with open(textfile, 'r') as f: 476 return convert_value(f.read(), ptype) 477 elif binfile is not None: 478 try: 479 from xmlrpc.client import Binary 480 except ImportError: 481 from xmlrpclib import Binary 482 with open(binfile, 'rb') as f: 483 return Binary(f.read()) 484 elif command is not None: 485 try: 486 if type(command) == unicode: 487 command = command.encode('utf-8') #attempt to force to string for shlex/subprocess 488 except NameError: 489 pass 490 if verbose: 491 print("... executing command param [%s]" % command) 492 import subprocess, shlex #shlex rocks 493 try: 494 p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) 495 c_value = p.communicate()[0] 496 if not isinstance(c_value, str): 497 c_value = c_value.decode('utf-8') 498 if p.returncode != 0: 499 raise ValueError("Cannot load command parameter [%s]: command [%s] returned with code [%s]"%(name, command, p.returncode)) 500 except OSError as e: 501 if e.errno == errno.ENOENT: 502 raise ValueError("Cannot load command parameter [%s]: no such command [%s]"%(name, command)) 503 raise 504 if c_value is None: 505 raise ValueError("parameter: unable to get output of command [%s]"%command) 506 return convert_value(c_value, ptype) 507 else: #_param_tag prevalidates, so this should not be reachable 508 raise ValueError("unable to determine parameter value")
509