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

Source Code for Module roshlaunch.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 10636 2010-08-10 16:53:55Z 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 
 55  yaml = None 
 56   
57 -class LoadException(RLException):
58 """Error loading data as specified (e.g. cannot find included files, etc...)""" 59 pass
60 61 #TODO: lists, maps(?)
62 -def convert_value(value, type_):
63 """ 64 Convert a value from a string representation into the specified 65 type 66 67 @param value: string representation of value 68 @type value: str 69 @param type_: int, double, string, bool, or auto 70 @type type_: str 71 @raise ValueError: if parameters are invalid 72 """ 73 type_ = type_.lower() 74 # currently don't support XML-RPC date, dateTime, maps, or list 75 # types 76 if type_ == 'auto': 77 #attempt numeric conversion 78 try: 79 if '.' in value: 80 return float(value) 81 else: 82 return int(value) 83 except ValueError, e: 84 pass 85 #bool 86 lval = value.lower() 87 if lval == 'true' or lval == 'false': 88 return convert_value(value, 'bool') 89 #string 90 return value 91 elif type_ == 'str' or type_ == 'string': 92 return value 93 elif type_ == 'int': 94 return int(value) 95 elif type_ == 'double': 96 return float(value) 97 elif type_ == 'bool' or type_ == 'boolean': 98 value = value.lower() 99 if value == 'true' or value == '1': 100 return True 101 elif value == 'false' or value == '0': 102 return False 103 raise ValueError("%s is not a '%s' type"%(value, type_)) 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 = 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
183 - def add_param(self, p):
184 """ 185 Add a ~param to the context. ~params are evaluated by any node 186 declarations that occur later in the same context. 187 188 @param p: parameter 189 @type p: L{Param} 190 """ 191 192 # override any params already set 193 matches = [m for m in self.params if m.key == p.key] 194 for m in matches: 195 self.params.remove(m) 196 self.params.append(p)
197
198 - def add_remap(self, remap):
199 """ 200 Add a new remap setting to the context. if a remap already 201 exists with the same from key, it will be removed 202 203 @param remap: remap setting 204 @type remap: (str, str) 205 """ 206 remap = [canonicalize_name(x) for x in remap] 207 if not remap[0] or not remap[1]: 208 raise RLException("remap from/to attributes cannot be empty") 209 if not is_legal_name(remap[0]): 210 raise RLException("remap from [%s] is not a valid ROS name"%remap[0]) 211 if not is_legal_name(remap[1]): 212 raise RLException("remap to [%s] is not a valid ROS name"%remap[1]) 213 214 matches = [r for r in self._remap_args if r[0] == remap[0]] 215 for m in matches: 216 self._remap_args.remove(m) 217 self._remap_args.append(remap)
218
219 - def add_arg(self, name, default=None, value=None):
220 """ 221 Add 'arg' to existing context. Args are only valid for their immediate context. 222 """ 223 if name in self.arg_names: 224 raise LoadException("arg '%s' has already been declared"%name) 225 self.arg_names.append(name) 226 227 resolve_dict = self.resolve_dict if self.include_resolve_dict is None else self.include_resolve_dict 228 229 if not 'arg' in resolve_dict: 230 resolve_dict['arg'] = {} 231 arg_dict = resolve_dict['arg'] 232 233 # args can only be declared once. they must also have one and 234 # only value at the time that they are declared. 235 if value is not None: 236 # value is set, error if declared in our arg dict as args 237 # with set values are constant/grounded. 238 if name in arg_dict: 239 raise LoadException("cannot override arg '%s', which has already been set"%name) 240 arg_dict[name] = value 241 elif default is not None: 242 # assign value if not in context 243 if name not in arg_dict: 244 arg_dict[name] = default 245 else: 246 # no value or default: appending to arg_names is all we 247 # need to do as it declares the existence of the arg. 248 pass
249
250 - def remap_args(self):
251 """ 252 @return: copy of the current remap arguments 253 @rtype: [(str, str)] 254 """ 255 if self.parent: 256 args = [] 257 # filter out any parent remap args that have the same from key 258 for pr in self.parent.remap_args(): 259 if not [r for r in self._remap_args if r[0] == pr[0]]: 260 args.append(pr) 261 args.extend(self._remap_args) 262 return args 263 return self._remap_args[:]
264
265 - def include_child(self, ns, filename):
266 """ 267 Create child namespace based on include inheritance rules 268 @param ns: sub-namespace of child context, or None if the 269 child context shares the same namespace 270 @type ns: str 271 @param filename: name of include file 272 @type filename: str 273 @return: A child xml context that inherits from this context 274 @rtype: L{LoaderContext}jj 275 """ 276 ctx = self.child(ns) 277 # arg declarations are reset across include boundaries 278 ctx.arg_names = [] 279 ctx.filename = filename 280 # keep the resolve_dict for now, we will do all new assignments into include_resolve_dict 281 ctx.include_resolve_dict = {} 282 #del ctx.resolve_dict['arg'] 283 return ctx
284
285 - def child(self, ns):
286 """ 287 @param ns: sub-namespace of child context, or None if the 288 child context shares the same namespace 289 @type ns: str 290 @return: A child xml context that inherits from this context 291 @rtype: L{LoaderContext} 292 """ 293 if ns: 294 if ns[0] == '/': # global (discouraged) 295 child_ns = ns 296 elif ns == PRIV_NAME: # ~name 297 # private names can only be scoped privately or globally 298 child_ns = PRIV_NAME 299 else: 300 child_ns = ns_join(self.ns, ns) 301 else: 302 child_ns = self.ns 303 return LoaderContext(child_ns, self.filename, parent=self, 304 params=self.params, env_args=self.env_args[:], 305 resolve_dict=deepcopy(self.resolve_dict), 306 arg_names=self.arg_names[:], include_resolve_dict=self.include_resolve_dict)
307 308 #TODO: in-progress refactorization. I'm slowly separating out 309 #non-XML-specific logic from xmlloader and moving into Loader. Soon 310 #this will mean that it will be easier to write coverage tests for 311 #lower-level logic. 312
313 -class Loader(object):
314 """ 315 Lower-level library for loading ROS launch model. It provides an 316 abstraction between the representation (e.g. XML) and the 317 validation of the property values. 318 """ 319
320 - def add_param(self, ros_config, param_name, param_value, verbose=True):
321 """ 322 Add L{Param} instances to launch config. Dictionary values are 323 unrolled into individual parameters. 324 325 @param ros_config: launch configuration 326 @type ros_config: L{ROSLaunchConfig} 327 @param param_name: name of parameter namespace to load values 328 into. If param_name is '/', param_value must be a dictionary 329 @type param_name: str 330 @param param_value: value to assign to param_name. If 331 param_value is a dictionary, it's values will be unrolled 332 into individual parameters. 333 @type param_value: str 334 @raise ValueError: if parameters cannot be processed into valid Params 335 """ 336 337 # shouldn't ever happen 338 if not param_name: 339 raise ValueError("no parameter name specified") 340 341 if param_name == '/' and type(param_value) != dict: 342 raise ValueError("Cannot load non-dictionary types into global namespace '/'") 343 344 if type(param_value) == dict: 345 # unroll params 346 for k, v in param_value.iteritems(): 347 self.add_param(ros_config, ns_join(param_name, k), v, verbose=verbose) 348 else: 349 ros_config.add_param(Param(param_name, param_value), verbose=verbose)
350
351 - def load_rosparam(self, context, ros_config, cmd, param, file, text, verbose=True):
352 """ 353 Load rosparam setting 354 355 @param context: Loader context 356 @type context: L{LoaderContext} 357 @param ros_config: launch configuration 358 @type ros_config: L{ROSLaunchConfig} 359 @param cmd: 'load', 'dump', or 'delete' 360 @type cmd: str 361 @param file: filename for rosparam to use or None 362 @type file: str 363 @param text: text for rosparam to load. Ignored if file is set. 364 @type text: str 365 @raise ValueError: if parameters cannot be processed into valid rosparam setting 366 """ 367 if not cmd in ('load', 'dump', 'delete'): 368 raise ValueError("command must be 'load', 'dump', or 'delete'") 369 if file is not None: 370 if cmd == 'load' and not os.path.isfile(file): 371 raise ValueError("file does not exist [%s]"%file) 372 if cmd == 'delete': 373 raise ValueError("'file' attribute is invalid with 'delete' command.") 374 375 full_param = ns_join(context.ns, param) if param else context.ns 376 377 if cmd == 'dump': 378 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, file, full_param), PHASE_SETUP)) 379 elif cmd == 'delete': 380 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, full_param), PHASE_SETUP)) 381 elif cmd == 'load': 382 # load YAML text 383 if file: 384 with open(file, 'r') as f: 385 text = f.read() 386 387 if not text: 388 if file: 389 raise ValueError("no YAML in file %s"%file) 390 else: 391 raise ValueError("no YAML to load") 392 393 # parse YAML text 394 # - lazy import 395 global yaml 396 if yaml is None: 397 import yaml 398 try: 399 data = yaml.load(text) 400 except yaml.MarkedYAMLError, e: 401 if not file: 402 raise ValueError("Error within YAML block:\n\t%s\n\nYAML is:\n%s"%(str(e), text)) 403 else: 404 raise ValueError("file %s contains invalid YAML:\n%s"%(file, str(e))) 405 except Exception, e: 406 if not file: 407 raise ValueError("invalid YAML: %s\n\nYAML is:\n%s"%(str(e), text)) 408 else: 409 raise ValueError("file %s contains invalid YAML:\n%s"%(file, str(e))) 410 411 # 'param' attribute is required for non-dictionary types 412 if not param and type(data) != dict: 413 raise ValueError("'param' attribute must be set for non-dictionary values") 414 415 self.add_param(ros_config, full_param, data, verbose=verbose) 416 417 else: 418 raise ValueError("unknown command %s"%cmd)
419 420
421 - def load_env(self, context, ros_config, name, value):
422 """ 423 Load environment variable setting 424 425 @param context: Loader context 426 @type context: L{LoaderContext} 427 @param ros_config: launch configuration 428 @type ros_config: L{ROSLaunchConfig} 429 @param name: environment variable name 430 @type name: str 431 @param value: environment variable value 432 @type value: str 433 """ 434 if not name: 435 raise ValueError("'name' attribute must be non-empty") 436 context.env_args.append((name, value))
437 438
439 - def param_value(self, verbose, name, ptype, value, textfile, binfile, command):
440 """ 441 Parse text representation of param spec into Python value 442 @param name: param name, for error message use only 443 @type name: str 444 @param verbose: print verbose output 445 @type verbose: bool 446 @param textfile: name of text file to load from, or None 447 @type textfile: str 448 @param binfile: name of binary file to load from, or None 449 @type binfile: str 450 @param command: command to execute for parameter value, or None 451 @type command: str 452 @raise ValueError: if parameters are invalid 453 """ 454 if value is not None: 455 return convert_value(value.strip(), ptype) 456 elif textfile is not None: 457 with open(textfile, 'r') as f: 458 return f.read() 459 elif binfile is not None: 460 import xmlrpclib 461 with open(binfile, 'rb') as f: 462 return xmlrpclib.Binary(f.read()) 463 elif command is not None: 464 if type(command) == unicode: 465 command = command.encode('UTF-8') #attempt to force to string for shlex/subprocess 466 if verbose: 467 print "... executing command param [%s]"%command 468 import subprocess, shlex #shlex rocks 469 try: 470 p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) 471 c_value = p.communicate()[0] 472 if p.returncode != 0: 473 raise ValueError("Cannot load command parameter [%s]: command [%s] returned with code [%s]"%(name, command, p.returncode)) 474 except OSError, (errno, strerr): 475 if errno == 2: 476 raise ValueError("Cannot load command parameter [%s]: no such command [%s]"%(name, command)) 477 raise 478 if c_value is None: 479 raise ValueError("parameter: unable to get output of command [%s]"%command) 480 return c_value 481 else: #_param_tag prevalidates, so this should not be reachable 482 raise ValueError("unable to determine parameter value")
483