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: loader.py 14270 2011-07-12 21:36:39Z kwc $ 
 34   
 35  """ 
 36  General routines and representations for loading roslaunch model. 
 37  """ 
 38   
 39  from __future__ import with_statement 
 40   
 41  import os 
 42  import sys 
 43  from copy import deepcopy 
 44   
 45  from roslaunch.core import Param, RosbinExecutable, Node, Test, Machine, \ 
 46      RLException, PHASE_SETUP 
 47   
 48  try: 
 49      from roslib.names import make_global_ns, ns_join, PRIV_NAME, load_mappings, is_legal_name, canonicalize_name 
 50  except ImportError: 
 51      raise ImportError('Cannot import new roslib libraries.\nThis is probably due to incompatible "roslib" directories on your PYTHONPATH.\nPlease check your PYTHONPATH and try again') 
 52   
 53   
 54  #lazy-import global for yaml and rosparam 
 55  yaml = None 
 56  rosparam = None 
 57   
58 -class LoadException(RLException):
59 """Error loading data as specified (e.g. cannot find included files, etc...)""" 60 pass
61 62 #TODO: lists, maps(?)
63 -def convert_value(value, type_):
64 """ 65 Convert a value from a string representation into the specified 66 type 67 68 @param value: string representation of value 69 @type value: str 70 @param type_: int, double, string, bool, or auto 71 @type type_: str 72 @raise ValueError: if parameters are invalid 73 """ 74 type_ = type_.lower() 75 # currently don't support XML-RPC date, dateTime, maps, or list 76 # types 77 if type_ == 'auto': 78 #attempt numeric conversion 79 try: 80 if '.' in value: 81 return float(value) 82 else: 83 return int(value) 84 except ValueError, e: 85 pass 86 #bool 87 lval = value.lower() 88 if lval == 'true' or lval == 'false': 89 return convert_value(value, 'bool') 90 #string 91 return value 92 elif type_ == 'str' or type_ == 'string': 93 return value 94 elif type_ == 'int': 95 return int(value) 96 elif type_ == 'double': 97 return float(value) 98 elif type_ == 'bool' or type_ == 'boolean': 99 value = value.lower() 100 if value == 'true' or value == '1': 101 return True 102 elif value == 'false' or value == '0': 103 return False 104 raise ValueError("%s is not a '%s' type"%(value, type_)) 105 else: 106 raise ValueError("Unknown type '%s'"%type_)
107
108 -def process_include_args(context):
109 """ 110 Processes arg declarations in context and makes sure that they are 111 properly declared for passing into an included file. Also will 112 correctly setup the context for passing to the included file. 113 """ 114 115 # make sure all arguments have values. arg_names and resolve_dict 116 # are cleared when the context was initially created. 117 arg_dict = context.include_resolve_dict.get('arg', {}) 118 for arg in context.arg_names: 119 if not arg in arg_dict: 120 raise LoadException("include args must have declared values") 121 122 # save args that were passed so we can check for unused args in post-processing 123 context.args_passed = arg_dict.keys()[:] 124 # clear arg declarations so included file can re-declare 125 context.arg_names = [] 126 127 # swap in new resolve dict for passing 128 context.resolve_dict = context.include_resolve_dict 129 context.include_resolve_dict = None
130
131 -def post_process_include_args(context):
132 bad = [a for a in context.args_passed if a not in context.arg_names] 133 if bad: 134 raise LoadException("unused args [%s] for include of [%s]"%(', '.join(bad), context.filename))
135
136 -def load_sysargs_into_context(context, argv):
137 """ 138 Load in ROS remapping arguments as arg assignments for context. 139 140 @param context: context to load into. context's resolve_dict for 'arg' will be reinitialized with values. 141 @type context: L{LoaderContext{ 142 @param argv: command-line arguments 143 @type argv: [str] 144 """ 145 # we use same command-line spec as ROS nodes 146 mappings = load_mappings(argv) 147 context.resolve_dict['arg'] = mappings
148
149 -class LoaderContext(object):
150 """ 151 Container for storing current loader context (e.g. namespace, 152 local parameter state, remapping state). 153 """ 154
155 - def __init__(self, ns, filename, parent=None, params=None, env_args=None, \ 156 resolve_dict=None, include_resolve_dict=None, arg_names=None):
157 """ 158 @param ns: namespace 159 @type ns: str 160 @param filename: name of file this is being loaded from 161 @type filename: str 162 @param resolve_dict: (optional) resolution dictionary for substitution args 163 @type resolve_dict: dict 164 @param include_resolve_dict: special resolution dictionary for 165 <include> tags. Must be None if this is not an <include> 166 context. 167 @type include_resolve_dict: dict 168 @param arg_names: name of args that have been declared in this context 169 @type arg_names: [str] 170 """ 171 self.parent = parent 172 self.ns = make_global_ns(ns or '/') 173 self._remap_args = [] 174 self.params = params or [] 175 self.env_args = env_args or [] 176 self.filename = filename 177 # for substitution args 178 self.resolve_dict = resolve_dict or {} 179 # arg names. Args that have been declared in this context 180 self.arg_names = arg_names or [] 181 # special scoped resolve dict for processing in <include> tag 182 self.include_resolve_dict = include_resolve_dict or None
183
184 - def add_param(self, p):
185 """ 186 Add a ~param to the context. ~params are evaluated by any node 187 declarations that occur later in the same context. 188 189 @param p: parameter 190 @type p: L{Param} 191 """ 192 193 # override any params already set 194 matches = [m for m in self.params if m.key == p.key] 195 for m in matches: 196 self.params.remove(m) 197 self.params.append(p)
198
199 - def add_remap(self, remap):
200 """ 201 Add a new remap setting to the context. if a remap already 202 exists with the same from key, it will be removed 203 204 @param remap: remap setting 205 @type remap: (str, str) 206 """ 207 remap = [canonicalize_name(x) for x in remap] 208 if not remap[0] or not remap[1]: 209 raise RLException("remap from/to attributes cannot be empty") 210 if not is_legal_name(remap[0]): 211 raise RLException("remap from [%s] is not a valid ROS name"%remap[0]) 212 if not is_legal_name(remap[1]): 213 raise RLException("remap to [%s] is not a valid ROS name"%remap[1]) 214 215 matches = [r for r in self._remap_args if r[0] == remap[0]] 216 for m in matches: 217 self._remap_args.remove(m) 218 self._remap_args.append(remap)
219
220 - def add_arg(self, name, default=None, value=None):
221 """ 222 Add 'arg' to existing context. Args are only valid for their immediate context. 223 """ 224 if name in self.arg_names: 225 raise LoadException("arg '%s' has already been declared"%name) 226 self.arg_names.append(name) 227 228 resolve_dict = self.resolve_dict if self.include_resolve_dict is None else self.include_resolve_dict 229 230 if not 'arg' in resolve_dict: 231 resolve_dict['arg'] = {} 232 arg_dict = resolve_dict['arg'] 233 234 # args can only be declared once. they must also have one and 235 # only value at the time that they are declared. 236 if value is not None: 237 # value is set, error if declared in our arg dict as args 238 # with set values are constant/grounded. 239 if name in arg_dict: 240 raise LoadException("cannot override arg '%s', which has already been set"%name) 241 arg_dict[name] = value 242 elif default is not None: 243 # assign value if not in context 244 if name not in arg_dict: 245 arg_dict[name] = default 246 else: 247 # no value or default: appending to arg_names is all we 248 # need to do as it declares the existence of the arg. 249 pass
250
251 - def remap_args(self):
252 """ 253 @return: copy of the current remap arguments 254 @rtype: [(str, str)] 255 """ 256 if self.parent: 257 args = [] 258 # filter out any parent remap args that have the same from key 259 for pr in self.parent.remap_args(): 260 if not [r for r in self._remap_args if r[0] == pr[0]]: 261 args.append(pr) 262 args.extend(self._remap_args) 263 return args 264 return self._remap_args[:]
265
266 - def include_child(self, ns, filename):
267 """ 268 Create child namespace based on include inheritance rules 269 @param ns: sub-namespace of child context, or None if the 270 child context shares the same namespace 271 @type ns: str 272 @param filename: name of include file 273 @type filename: str 274 @return: A child xml context that inherits from this context 275 @rtype: L{LoaderContext}jj 276 """ 277 ctx = self.child(ns) 278 # arg declarations are reset across include boundaries 279 ctx.arg_names = [] 280 ctx.filename = filename 281 # keep the resolve_dict for now, we will do all new assignments into include_resolve_dict 282 ctx.include_resolve_dict = {} 283 #del ctx.resolve_dict['arg'] 284 return ctx
285
286 - def child(self, ns):
287 """ 288 @param ns: sub-namespace of child context, or None if the 289 child context shares the same namespace 290 @type ns: str 291 @return: A child xml context that inherits from this context 292 @rtype: L{LoaderContext} 293 """ 294 if ns: 295 if ns[0] == '/': # global (discouraged) 296 child_ns = ns 297 elif ns == PRIV_NAME: # ~name 298 # private names can only be scoped privately or globally 299 child_ns = PRIV_NAME 300 else: 301 child_ns = ns_join(self.ns, ns) 302 else: 303 child_ns = self.ns 304 return LoaderContext(child_ns, self.filename, parent=self, 305 params=self.params, env_args=self.env_args[:], 306 resolve_dict=deepcopy(self.resolve_dict), 307 arg_names=self.arg_names[:], include_resolve_dict=self.include_resolve_dict)
308 309 #TODO: in-progress refactorization. I'm slowly separating out 310 #non-XML-specific logic from xmlloader and moving into Loader. Soon 311 #this will mean that it will be easier to write coverage tests for 312 #lower-level logic. 313
314 -class Loader(object):
315 """ 316 Lower-level library for loading ROS launch model. It provides an 317 abstraction between the representation (e.g. XML) and the 318 validation of the property values. 319 """ 320
321 - def add_param(self, ros_config, param_name, param_value, verbose=True):
322 """ 323 Add L{Param} instances to launch config. Dictionary values are 324 unrolled into individual parameters. 325 326 @param ros_config: launch configuration 327 @type ros_config: L{ROSLaunchConfig} 328 @param param_name: name of parameter namespace to load values 329 into. If param_name is '/', param_value must be a dictionary 330 @type param_name: str 331 @param param_value: value to assign to param_name. If 332 param_value is a dictionary, it's values will be unrolled 333 into individual parameters. 334 @type param_value: str 335 @raise ValueError: if parameters cannot be processed into valid Params 336 """ 337 338 # shouldn't ever happen 339 if not param_name: 340 raise ValueError("no parameter name specified") 341 342 if param_name == '/' and type(param_value) != dict: 343 raise ValueError("Cannot load non-dictionary types into global namespace '/'") 344 345 if type(param_value) == dict: 346 # unroll params 347 for k, v in param_value.iteritems(): 348 self.add_param(ros_config, ns_join(param_name, k), v, verbose=verbose) 349 else: 350 ros_config.add_param(Param(param_name, param_value), verbose=verbose)
351
352 - def load_rosparam(self, context, ros_config, cmd, param, file_, text, verbose=True):
353 """ 354 Load rosparam setting 355 356 @param context: Loader context 357 @type context: L{LoaderContext} 358 @param ros_config: launch configuration 359 @type ros_config: L{ROSLaunchConfig} 360 @param cmd: 'load', 'dump', or 'delete' 361 @type cmd: str 362 @param file_: filename for rosparam to use or None 363 @type file_: str 364 @param text: text for rosparam to load. Ignored if file_ is set. 365 @type text: str 366 @raise ValueError: if parameters cannot be processed into valid rosparam setting 367 """ 368 if not cmd in ('load', 'dump', 'delete'): 369 raise ValueError("command must be 'load', 'dump', or 'delete'") 370 if file_ is not None: 371 if cmd == 'load' and not os.path.isfile(file_): 372 raise ValueError("file does not exist [%s]"%file_) 373 if cmd == 'delete': 374 raise ValueError("'file' attribute is invalid with 'delete' command.") 375 376 full_param = ns_join(context.ns, param) if param else context.ns 377 378 if cmd == 'dump': 379 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, file_, full_param), PHASE_SETUP)) 380 elif cmd == 'delete': 381 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, full_param), PHASE_SETUP)) 382 elif cmd == 'load': 383 # load YAML text 384 if file_: 385 with open(file_, 'r') as f: 386 text = f.read() 387 388 # parse YAML text 389 # - lazy import 390 global yaml 391 if yaml is None: 392 import yaml 393 # - lazy import: we have to import rosparam in oder to to configure the YAML constructors 394 global rosparam 395 if rosparam is None: 396 import rosparam 397 try: 398 data = yaml.load(text) 399 # #3162: if there is no YAML, load() will return an 400 # empty string. We want an empty dictionary instead 401 # for our representation of empty. 402 if data is None: 403 data = {} 404 except yaml.MarkedYAMLError, e: 405 if not file_: 406 raise ValueError("Error within YAML block:\n\t%s\n\nYAML is:\n%s"%(str(e), text)) 407 else: 408 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 409 except Exception, e: 410 if not file_: 411 raise ValueError("invalid YAML: %s\n\nYAML is:\n%s"%(str(e), text)) 412 else: 413 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 414 415 # 'param' attribute is required for non-dictionary types 416 if not param and type(data) != dict: 417 raise ValueError("'param' attribute must be set for non-dictionary values") 418 419 self.add_param(ros_config, full_param, data, verbose=verbose) 420 421 else: 422 raise ValueError("unknown command %s"%cmd)
423 424
425 - def load_env(self, context, ros_config, name, value):
426 """ 427 Load environment variable setting 428 429 @param context: Loader context 430 @type context: L{LoaderContext} 431 @param ros_config: launch configuration 432 @type ros_config: L{ROSLaunchConfig} 433 @param name: environment variable name 434 @type name: str 435 @param value: environment variable value 436 @type value: str 437 """ 438 if not name: 439 raise ValueError("'name' attribute must be non-empty") 440 context.env_args.append((name, value))
441 442
443 - def param_value(self, verbose, name, ptype, value, textfile, binfile, command):
444 """ 445 Parse text representation of param spec into Python value 446 @param name: param name, for error message use only 447 @type name: str 448 @param verbose: print verbose output 449 @type verbose: bool 450 @param textfile: name of text file to load from, or None 451 @type textfile: str 452 @param binfile: name of binary file to load from, or None 453 @type binfile: str 454 @param command: command to execute for parameter value, or None 455 @type command: str 456 @raise ValueError: if parameters are invalid 457 """ 458 if value is not None: 459 return convert_value(value.strip(), ptype) 460 elif textfile is not None: 461 with open(textfile, 'r') as f: 462 return f.read() 463 elif binfile is not None: 464 import xmlrpclib 465 with open(binfile, 'rb') as f: 466 return xmlrpclib.Binary(f.read()) 467 elif command is not None: 468 if type(command) == unicode: 469 command = command.encode('UTF-8') #attempt to force to string for shlex/subprocess 470 if verbose: 471 print "... executing command param [%s]"%command 472 import subprocess, shlex #shlex rocks 473 try: 474 p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) 475 c_value = p.communicate()[0] 476 if p.returncode != 0: 477 raise ValueError("Cannot load command parameter [%s]: command [%s] returned with code [%s]"%(name, command, p.returncode)) 478 except OSError, (errno, strerr): 479 if errno == 2: 480 raise ValueError("Cannot load command parameter [%s]: no such command [%s]"%(name, command)) 481 raise 482 if c_value is None: 483 raise ValueError("parameter: unable to get output of command [%s]"%command) 484 return c_value 485 else: #_param_tag prevalidates, so this should not be reachable 486 raise ValueError("unable to determine parameter value")
487