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 as 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 = list(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 # when this context was created via include, was pass_all_args set? 176 self.pass_all_args = False
177
178 - def add_param(self, p):
179 """ 180 Add a ~param to the context. ~params are evaluated by any node 181 declarations that occur later in the same context. 182 183 @param p: parameter 184 @type p: L{Param} 185 """ 186 187 # override any params already set 188 matches = [m for m in self.params if m.key == p.key] 189 for m in matches: 190 self.params.remove(m) 191 self.params.append(p)
192
193 - def add_remap(self, remap):
194 """ 195 Add a new remap setting to the context. if a remap already 196 exists with the same from key, it will be removed 197 198 @param remap: remap setting 199 @type remap: (str, str) 200 """ 201 remap = [canonicalize_name(x) for x in remap] 202 if not remap[0] or not remap[1]: 203 raise RLException("remap from/to attributes cannot be empty") 204 if not is_legal_name(remap[0]): 205 raise RLException("remap from [%s] is not a valid ROS name"%remap[0]) 206 if not is_legal_name(remap[1]): 207 raise RLException("remap to [%s] is not a valid ROS name"%remap[1]) 208 209 matches = [r for r in self._remap_args if r[0] == remap[0]] 210 for m in matches: 211 self._remap_args.remove(m) 212 self._remap_args.append(remap)
213
214 - def add_arg(self, name, default=None, value=None, doc=None):
215 """ 216 Add 'arg' to existing context. Args are only valid for their immediate context. 217 """ 218 if name in self.arg_names: 219 # Ignore the duplication if pass_all_args was set 220 if not self.pass_all_args: 221 raise LoadException("arg '%s' has already been declared"%name) 222 else: 223 self.arg_names.append(name) 224 225 resolve_dict = self.resolve_dict if self.include_resolve_dict is None else self.include_resolve_dict 226 227 if not 'arg' in resolve_dict: 228 resolve_dict['arg'] = {} 229 arg_dict = resolve_dict['arg'] 230 231 # args can only be declared once. they must also have one and 232 # only value at the time that they are declared. 233 if value is not None: 234 # value is set, error if declared in our arg dict as args 235 # with set values are constant/grounded. 236 # But don't error if pass_all_args was used to include this 237 # context; rather just override the passed-in value. 238 if name in arg_dict and not self.pass_all_args: 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 # add arg documentation string dict if it doesn't exist yet and if it can be used 251 if not 'arg_doc' in resolve_dict: 252 resolve_dict['arg_doc'] = {} 253 arg_doc_dict = resolve_dict['arg_doc'] 254 255 if not value: 256 # store the documentation for this argument 257 arg_doc_dict[name] = (doc, default)
258 259
260 - def remap_args(self):
261 """ 262 @return: copy of the current remap arguments 263 @rtype: [(str, str)] 264 """ 265 if self.parent: 266 args = [] 267 # filter out any parent remap args that have the same from key 268 for pr in self.parent.remap_args(): 269 if not [r for r in self._remap_args if r[0] == pr[0]]: 270 args.append(pr) 271 args.extend(self._remap_args) 272 return args 273 return self._remap_args[:]
274
275 - def include_child(self, ns, filename):
276 """ 277 Create child namespace based on include inheritance rules 278 @param ns: sub-namespace of child context, or None if the 279 child context shares the same namespace 280 @type ns: str 281 @param filename: name of include file 282 @type filename: str 283 @return: A child xml context that inherits from this context 284 @rtype: L{LoaderContext}jj 285 """ 286 ctx = self.child(ns) 287 # arg declarations are reset across include boundaries 288 ctx.arg_names = [] 289 ctx.filename = filename 290 # keep the resolve_dict for now, we will do all new assignments into include_resolve_dict 291 ctx.include_resolve_dict = {} 292 #del ctx.resolve_dict['arg'] 293 return ctx
294
295 - def child(self, ns):
296 """ 297 @param ns: sub-namespace of child context, or None if the 298 child context shares the same namespace 299 @type ns: str 300 @return: A child xml context that inherits from this context 301 @rtype: L{LoaderContext} 302 """ 303 if ns: 304 if ns[0] == '/': # global (discouraged) 305 child_ns = ns 306 elif ns == PRIV_NAME: # ~name 307 # private names can only be scoped privately or globally 308 child_ns = PRIV_NAME 309 else: 310 child_ns = ns_join(self.ns, ns) 311 else: 312 child_ns = self.ns 313 return LoaderContext(child_ns, self.filename, parent=self, 314 params=self.params, env_args=self.env_args[:], 315 resolve_dict=deepcopy(self.resolve_dict), 316 arg_names=self.arg_names[:], include_resolve_dict=self.include_resolve_dict)
317 318 #TODO: in-progress refactorization. I'm slowly separating out 319 #non-XML-specific logic from xmlloader and moving into Loader. Soon 320 #this will mean that it will be easier to write coverage tests for 321 #lower-level logic. 322
323 -class Loader(object):
324 """ 325 Lower-level library for loading ROS launch model. It provides an 326 abstraction between the representation (e.g. XML) and the 327 validation of the property values. 328 """ 329
330 - def add_param(self, ros_config, param_name, param_value, verbose=True):
331 """ 332 Add L{Param} instances to launch config. Dictionary values are 333 unrolled into individual parameters. 334 335 @param ros_config: launch configuration 336 @type ros_config: L{ROSLaunchConfig} 337 @param param_name: name of parameter namespace to load values 338 into. If param_name is '/', param_value must be a dictionary 339 @type param_name: str 340 @param param_value: value to assign to param_name. If 341 param_value is a dictionary, it's values will be unrolled 342 into individual parameters. 343 @type param_value: str 344 @raise ValueError: if parameters cannot be processed into valid Params 345 """ 346 347 # shouldn't ever happen 348 if not param_name: 349 raise ValueError("no parameter name specified") 350 351 if param_name == '/' and type(param_value) != dict: 352 raise ValueError("Cannot load non-dictionary types into global namespace '/'") 353 354 if type(param_value) == dict: 355 # unroll params 356 for k, v in param_value.items(): 357 self.add_param(ros_config, ns_join(param_name, k), v, verbose=verbose) 358 else: 359 ros_config.add_param(Param(param_name, param_value), verbose=verbose)
360
361 - def load_rosparam(self, context, ros_config, cmd, param, file_, text, verbose=True):
362 """ 363 Load rosparam setting 364 365 @param context: Loader context 366 @type context: L{LoaderContext} 367 @param ros_config: launch configuration 368 @type ros_config: L{ROSLaunchConfig} 369 @param cmd: 'load', 'dump', or 'delete' 370 @type cmd: str 371 @param file_: filename for rosparam to use or None 372 @type file_: str 373 @param text: text for rosparam to load. Ignored if file_ is set. 374 @type text: str 375 @raise ValueError: if parameters cannot be processed into valid rosparam setting 376 """ 377 if not cmd in ('load', 'dump', 'delete'): 378 raise ValueError("command must be 'load', 'dump', or 'delete'") 379 if file_ is not None: 380 if cmd == 'load' and not os.path.isfile(file_): 381 raise ValueError("file does not exist [%s]"%file_) 382 if cmd == 'delete': 383 raise ValueError("'file' attribute is invalid with 'delete' command.") 384 385 full_param = ns_join(context.ns, param) if param else context.ns 386 387 if cmd == 'dump': 388 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, file_, full_param), PHASE_SETUP)) 389 elif cmd == 'delete': 390 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, full_param), PHASE_SETUP)) 391 elif cmd == 'load': 392 # load YAML text 393 if file_: 394 with open(file_, 'r') as f: 395 text = f.read() 396 397 # parse YAML text 398 # - lazy import 399 global yaml 400 if yaml is None: 401 import yaml 402 # - lazy import: we have to import rosparam in oder to to configure the YAML constructors 403 global rosparam 404 if rosparam is None: 405 import rosparam 406 try: 407 data = yaml.load(text) 408 # #3162: if there is no YAML, load() will return an 409 # empty string. We want an empty dictionary instead 410 # for our representation of empty. 411 if data is None: 412 data = {} 413 except yaml.MarkedYAMLError as e: 414 if not file_: 415 raise ValueError("Error within YAML block:\n\t%s\n\nYAML is:\n%s"%(str(e), text)) 416 else: 417 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 418 except Exception as e: 419 if not file_: 420 raise ValueError("invalid YAML: %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 424 # 'param' attribute is required for non-dictionary types 425 if not param and type(data) != dict: 426 raise ValueError("'param' attribute must be set for non-dictionary values") 427 428 self.add_param(ros_config, full_param, data, verbose=verbose) 429 430 else: 431 raise ValueError("unknown command %s"%cmd)
432 433
434 - def load_env(self, context, ros_config, name, value):
435 """ 436 Load environment variable setting 437 438 @param context: Loader context 439 @type context: L{LoaderContext} 440 @param ros_config: launch configuration 441 @type ros_config: L{ROSLaunchConfig} 442 @param name: environment variable name 443 @type name: str 444 @param value: environment variable value 445 @type value: str 446 """ 447 if not name: 448 raise ValueError("'name' attribute must be non-empty") 449 context.env_args.append((name, value))
450 451
452 - def param_value(self, verbose, name, ptype, value, textfile, binfile, command):
453 """ 454 Parse text representation of param spec into Python value 455 @param name: param name, for error message use only 456 @type name: str 457 @param verbose: print verbose output 458 @type verbose: bool 459 @param textfile: name of text file to load from, or None 460 @type textfile: str 461 @param binfile: name of binary file to load from, or None 462 @type binfile: str 463 @param command: command to execute for parameter value, or None 464 @type command: str 465 @raise ValueError: if parameters are invalid 466 """ 467 if value is not None: 468 return convert_value(value.strip(), ptype) 469 elif textfile is not None: 470 with open(textfile, 'r') as f: 471 return f.read() 472 elif binfile is not None: 473 try: 474 from xmlrpc.client import Binary 475 except ImportError: 476 from xmlrpclib import Binary 477 with open(binfile, 'rb') as f: 478 return Binary(f.read()) 479 elif command is not None: 480 try: 481 if type(command) == unicode: 482 command = command.encode('UTF-8') #attempt to force to string for shlex/subprocess 483 except NameError: 484 pass 485 if verbose: 486 print("... executing command param [%s]" % command) 487 import subprocess, shlex #shlex rocks 488 try: 489 p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) 490 c_value = p.communicate()[0] 491 if p.returncode != 0: 492 raise ValueError("Cannot load command parameter [%s]: command [%s] returned with code [%s]"%(name, command, p.returncode)) 493 except OSError as e: 494 if e.errno == 2: 495 raise ValueError("Cannot load command parameter [%s]: no such command [%s]"%(name, command)) 496 raise 497 if c_value is None: 498 raise ValueError("parameter: unable to get output of command [%s]"%command) 499 return c_value 500 else: #_param_tag prevalidates, so this should not be reachable 501 raise ValueError("unable to determine parameter value")
502