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, subst_function=None):
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 if subst_function is not None: 398 text = subst_function(text) 399 # parse YAML text 400 # - lazy import 401 global yaml 402 if yaml is None: 403 import yaml 404 # - lazy import: we have to import rosparam in oder to to configure the YAML constructors 405 global rosparam 406 if rosparam is None: 407 import rosparam 408 try: 409 data = yaml.load(text) 410 # #3162: if there is no YAML, load() will return an 411 # empty string. We want an empty dictionary instead 412 # for our representation of empty. 413 if data is None: 414 data = {} 415 except yaml.MarkedYAMLError as e: 416 if not file_: 417 raise ValueError("Error within YAML block:\n\t%s\n\nYAML is:\n%s"%(str(e), text)) 418 else: 419 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 420 except Exception as e: 421 if not file_: 422 raise ValueError("invalid YAML: %s\n\nYAML is:\n%s"%(str(e), text)) 423 else: 424 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 425 426 # 'param' attribute is required for non-dictionary types 427 if not param and type(data) != dict: 428 raise ValueError("'param' attribute must be set for non-dictionary values") 429 430 self.add_param(ros_config, full_param, data, verbose=verbose) 431 432 else: 433 raise ValueError("unknown command %s"%cmd)
434 435
436 - def load_env(self, context, ros_config, name, value):
437 """ 438 Load environment variable setting 439 440 @param context: Loader context 441 @type context: L{LoaderContext} 442 @param ros_config: launch configuration 443 @type ros_config: L{ROSLaunchConfig} 444 @param name: environment variable name 445 @type name: str 446 @param value: environment variable value 447 @type value: str 448 """ 449 if not name: 450 raise ValueError("'name' attribute must be non-empty") 451 context.env_args.append((name, value))
452 453
454 - def param_value(self, verbose, name, ptype, value, textfile, binfile, command):
455 """ 456 Parse text representation of param spec into Python value 457 @param name: param name, for error message use only 458 @type name: str 459 @param verbose: print verbose output 460 @type verbose: bool 461 @param textfile: name of text file to load from, or None 462 @type textfile: str 463 @param binfile: name of binary file to load from, or None 464 @type binfile: str 465 @param command: command to execute for parameter value, or None 466 @type command: str 467 @raise ValueError: if parameters are invalid 468 """ 469 if value is not None: 470 return convert_value(value.strip(), ptype) 471 elif textfile is not None: 472 with open(textfile, 'r') as f: 473 return f.read() 474 elif binfile is not None: 475 try: 476 from xmlrpc.client import Binary 477 except ImportError: 478 from xmlrpclib import Binary 479 with open(binfile, 'rb') as f: 480 return Binary(f.read()) 481 elif command is not None: 482 try: 483 if type(command) == unicode: 484 command = command.encode('utf-8') #attempt to force to string for shlex/subprocess 485 except NameError: 486 pass 487 if verbose: 488 print("... executing command param [%s]" % command) 489 import subprocess, shlex #shlex rocks 490 try: 491 p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) 492 c_value = p.communicate()[0] 493 if not isinstance(c_value, str): 494 c_value = c_value.decode('utf-8') 495 if p.returncode != 0: 496 raise ValueError("Cannot load command parameter [%s]: command [%s] returned with code [%s]"%(name, command, p.returncode)) 497 except OSError as e: 498 if e.errno == 2: 499 raise ValueError("Cannot load command parameter [%s]: no such command [%s]"%(name, command)) 500 raise 501 if c_value is None: 502 raise ValueError("parameter: unable to get output of command [%s]"%command) 503 return c_value 504 else: #_param_tag prevalidates, so this should not be reachable 505 raise ValueError("unable to determine parameter value")
506