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