__init__.py
Go to the documentation of this file.
00001 # Copyright (c) 2015, Open Source Robotics Foundation, Inc.
00002 # Copyright (c) 2013, Willow Garage, Inc.
00003 # All rights reserved.
00004 #
00005 # Redistribution and use in source and binary forms, with or without
00006 # modification, are permitted provided that the following conditions are met:
00007 #
00008 #     * Redistributions of source code must retain the above copyright
00009 #       notice, this list of conditions and the following disclaimer.
00010 #     * Redistributions in binary form must reproduce the above copyright
00011 #       notice, this list of conditions and the following disclaimer in the
00012 #       documentation and/or other materials provided with the distribution.
00013 #     * Neither the name of the Open Source Robotics Foundation, Inc.
00014 #       nor the names of its contributors may be used to endorse or promote
00015 #       products derived from this software without specific prior
00016 #       written permission.
00017 #
00018 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
00019 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00020 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00021 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
00022 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
00023 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
00024 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
00025 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
00026 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
00027 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00028 # POSSIBILITY OF SUCH DAMAGE.
00029 
00030 # Authors: Stuart Glaser, William Woodall, Robert Haschke
00031 # Maintainer: Morgan Quigley <morgan@osrfoundation.org>
00032 
00033 from __future__ import print_function, division
00034 
00035 import glob
00036 import os
00037 import re
00038 import sys
00039 import ast
00040 import math
00041 
00042 from roslaunch import substitution_args
00043 from rospkg.common import ResourceNotFound
00044 from copy import deepcopy
00045 from .color import warning, error, message
00046 from .xmlutils import *
00047 from .cli import process_args
00048 
00049 
00050 try:
00051     _basestr = basestring
00052 except NameError:
00053     _basestr = str
00054 
00055 # Dictionary of substitution args
00056 substitution_args_context = {}
00057 
00058 
00059 # Stack of currently processed files
00060 filestack = []
00061 
00062 def push_file(filename):
00063     """
00064     Push a new filename to the filestack.
00065     Instead of directly modifying filestack, a deep-copy is created and modified,
00066     while the old filestack is returned.
00067     This allows to store the filestack that was active when a macro or property is defined
00068     """
00069     global filestack
00070     oldstack = filestack
00071     filestack = deepcopy(filestack)
00072     filestack.append(filename)
00073     return oldstack
00074 
00075 def restore_filestack(oldstack):
00076     global filestack
00077     filestack = oldstack
00078 
00079 
00080 def abs_filename_spec(filename_spec):
00081     """
00082     Prepend the dirname of the currently processed file
00083     if filename_spec is not yet absolute
00084     """
00085     if not os.path.isabs(filename_spec):
00086         parent_filename = filestack[-1]
00087         basedir = os.path.dirname(parent_filename) if parent_filename else '.'
00088         return os.path.join(basedir, filename_spec)
00089     return filename_spec
00090 
00091 
00092 def load_yaml(filename):
00093     try:
00094         import yaml
00095     except:
00096         raise XacroException("yaml support not available; install python-yaml")
00097 
00098     filename = abs_filename_spec(filename)
00099     f = open(filename)
00100     oldstack = push_file(filename)
00101     try:
00102         return yaml.load(f)
00103     finally:
00104         f.close()
00105         restore_filestack(oldstack)
00106 
00107 
00108 # global symbols dictionary
00109 # taking simple security measures to forbid access to __builtins__
00110 # only the very few symbols explicitly listed are allowed
00111 # for discussion, see: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
00112 global_symbols = {'__builtins__': {k: __builtins__[k] for k in ['list', 'dict', 'map', 'str', 'float', 'int']}}
00113 # also define all math symbols and functions
00114 global_symbols.update(math.__dict__)
00115 # allow to import dicts from yaml
00116 global_symbols.update(dict(load_yaml=load_yaml))
00117 
00118 
00119 class XacroException(Exception):
00120     """
00121     XacroException allows to wrap another exception (exc) and to augment
00122     its error message: prefixing with msg and suffixing with suffix.
00123     str(e) finally prints: msg str(exc) suffix
00124     """
00125     def __init__(self, msg=None, suffix=None, exc=None, macro=None):
00126         super(XacroException, self).__init__(msg)
00127         self.suffix = suffix
00128         self.exc = exc
00129         self.macros = [] if macro is None else [macro]
00130 
00131     def __str__(self):
00132         return ' '.join([str(item) for item in
00133                          [self.message, self.exc, self.suffix] if item])
00134 
00135 
00136 verbosity = 1
00137 # deprecate non-namespaced use of xacro tags (issues #41, #59, #60)
00138 def deprecated_tag(_issued=[False]):
00139     if _issued[0]:
00140         return
00141     _issued[0] = True
00142 
00143     if verbosity > 0:
00144         warning("deprecated: xacro tags should be prepended with 'xacro' xml namespace.")
00145         message("""Use the following script to fix incorrect usage:
00146         find . -iname "*.xacro" | xargs sed -i 's#<\([/]\\?\)\(if\|unless\|include\|arg\|property\|macro\|insert_block\)#<\\1xacro:\\2#g'""")
00147         print_location(filestack)
00148         print(file=sys.stderr)
00149 
00150 
00151 # require xacro namespace?
00152 allow_non_prefixed_tags = True
00153 
00154 
00155 def check_deprecated_tag(tag_name):
00156     """
00157     Check whether tagName starts with xacro prefix. If not, issue a warning.
00158     :param tag_name:
00159     :return: True if tagName is accepted as xacro tag
00160              False if tagName doesn't start with xacro prefix, but the prefix is required
00161     """
00162     if tag_name.startswith('xacro:'):
00163         return True
00164     else:
00165         if allow_non_prefixed_tags:
00166             deprecated_tag()
00167         return allow_non_prefixed_tags
00168 
00169 
00170 class Macro(object):
00171     def __init__(self):
00172         self.body = None  # original xml.dom.Node
00173         self.params = []  # parsed parameter names
00174         self.defaultmap = {}  # default parameter values
00175         self.history = []  # definition history
00176 
00177 
00178 def eval_extension(s):
00179     if s == '$(cwd)':
00180         return os.getcwd()
00181     try:
00182         return substitution_args.resolve_args(s, context=substitution_args_context, resolve_anon=False)
00183     except substitution_args.ArgException as e:
00184         raise XacroException("Undefined substitution argument", exc=e)
00185     except ResourceNotFound as e:
00186         raise XacroException("resource not found:", exc=e)
00187 
00188 
00189 do_check_order=False
00190 class Table(object):
00191     def __init__(self, parent=None):
00192         self.parent = parent
00193         self.table = {}
00194         self.unevaluated = set()  # set of unevaluated variables
00195         self.recursive = []  # list of currently resolved vars (to resolve recursive definitions)
00196         # the following variables are for debugging / checking only
00197         self.depth = self.parent.depth + 1 if self.parent else 0
00198         if do_check_order:
00199             # this is for smooth transition from deprecated to --inorder processing
00200             self.used = set() # set of used properties
00201             self.redefined = dict() # set of properties redefined after usage
00202 
00203     @staticmethod
00204     def _eval_literal(value):
00205         if isinstance(value, _basestr):
00206             try:
00207                 # try to evaluate as literal, e.g. number, boolean, etc.
00208                 # this is needed to handle numbers in property definitions as numbers, not strings
00209                 evaluated = ast.literal_eval(value.strip())
00210                 # However, (simple) list, tuple, dict expressions will be evaluated as such too,
00211                 # which would break expected behaviour. Thus we only accept the evaluation otherwise.
00212                 if not isinstance(evaluated, (list, dict, tuple)):
00213                     return evaluated
00214             except:
00215                 pass
00216 
00217         return value
00218 
00219     def _resolve_(self, key):
00220         # lazy evaluation
00221         if key in self.unevaluated:
00222             if key in self.recursive:
00223                 raise XacroException("recursive variable definition: %s" %
00224                                      " -> ".join(self.recursive + [key]))
00225             self.recursive.append(key)
00226             self.table[key] = self._eval_literal(eval_text(self.table[key], self))
00227             self.unevaluated.remove(key)
00228             self.recursive.remove(key)
00229 
00230         # return evaluated result
00231         value = self.table[key]
00232         if (verbosity > 2 and self.parent is None) or verbosity > 3:
00233             print("{indent}use {key}: {value} ({loc})".format(
00234                 indent=self.depth*' ', key=key, value=value, loc=filestack[-1]), file=sys.stderr)
00235         if do_check_order:
00236             self.used.add(key)
00237         return value
00238 
00239     def __getitem__(self, key):
00240         if key in self.table:
00241             return self._resolve_(key)
00242         elif self.parent:
00243             return self.parent[key]
00244         else:
00245             raise KeyError(key)
00246 
00247     def _setitem(self, key, value, unevaluated):
00248         if do_check_order and key in self.used and key not in self.redefined:
00249             self.redefined[key] = filestack[-1]
00250 
00251         if key in global_symbols:
00252             warning("redefining global property: %s" % key)
00253             print_location(filestack)
00254 
00255         value = self._eval_literal(value)
00256         self.table[key] = value
00257         if unevaluated and isinstance(value, _basestr):
00258             # literal evaluation failed: re-evaluate lazily at first access
00259             self.unevaluated.add(key)
00260         elif key in self.unevaluated:
00261             # all other types cannot be evaluated
00262             self.unevaluated.remove(key)
00263         if (verbosity > 2 and self.parent is None) or verbosity > 3:
00264             print("{indent}set {key}: {value} ({loc})".format(
00265                 indent=self.depth*' ', key=key, value=value, loc=filestack[-1]), file=sys.stderr)
00266 
00267     def __setitem__(self, key, value):
00268         self._setitem(key, value, unevaluated=True)
00269 
00270     def __contains__(self, key):
00271         return \
00272             key in self.table or \
00273             (self.parent and key in self.parent)
00274 
00275     def __str__(self):
00276         s = str(self.table)
00277         if isinstance(self.parent, Table):
00278             s += "\n  parent: "
00279             s += str(self.parent)
00280         return s
00281 
00282     def root(self):
00283         p = self
00284         while p.parent:
00285             p = p.parent
00286         return p
00287 
00288 class NameSpace(object):
00289     # dot access (namespace.property) is forwarded to getitem()
00290     def __getattr__(self, item):
00291         return self.__getitem__(item)
00292 
00293 class PropertyNameSpace(Table, NameSpace):
00294     def __init__(self, parent=None):
00295         super(PropertyNameSpace, self).__init__(parent)
00296 
00297 class MacroNameSpace(dict, NameSpace):
00298     def __init__(self, *args, **kwargs):
00299         super(MacroNameSpace, self).__init__(*args, **kwargs)
00300 
00301 
00302 class QuickLexer(object):
00303     def __init__(self, *args, **kwargs):
00304         if args:
00305             # copy attributes + variables from other instance
00306             other = args[0]
00307             self.__dict__.update(other.__dict__)
00308         else:
00309             self.res = []
00310             for k, v in kwargs.items():
00311                 self.__setattr__(k, len(self.res))
00312                 self.res.append(re.compile(v))
00313         self.str = ""
00314         self.top = None
00315 
00316     def lex(self, str):
00317         self.str = str
00318         self.top = None
00319         self.next()
00320 
00321     def peek(self):
00322         return self.top
00323 
00324     def next(self):
00325         result = self.top
00326         self.top = None
00327         for i in range(len(self.res)):
00328             m = self.res[i].match(self.str)
00329             if m:
00330                 self.top = (i, m.group(0))
00331                 self.str = self.str[m.end():]
00332                 break
00333         return result
00334 
00335 
00336 all_includes = []
00337 
00338 include_no_matches_msg = """Include tag's filename spec \"{}\" matched no files."""
00339 
00340 
00341 def is_include(elt):
00342     # Xacro should not use plain 'include' tags but only namespaced ones. Causes conflicts with
00343     # other XML elements including Gazebo's <gazebo> extensions
00344     if elt.tagName not in ['xacro:include', 'include']:
00345         return False
00346 
00347     # Temporary fix for ROS Hydro and the xacro include scope problem
00348     if elt.tagName == 'include':
00349         # check if there is any element within the <include> tag. mostly we are concerned
00350         # with Gazebo's <uri> element, but it could be anything. also, make sure the child
00351         # nodes aren't just a single Text node, which is still considered a deprecated
00352         # instance
00353         if elt.childNodes and not (len(elt.childNodes) == 1 and
00354                                    elt.childNodes[0].nodeType == elt.TEXT_NODE):
00355             # this is not intended to be a xacro element, so we can ignore it
00356             return False
00357         else:
00358             # throw a deprecated warning
00359             return check_deprecated_tag(elt.tagName)
00360     return True
00361 
00362 
00363 def get_include_files(filename_spec, symbols):
00364     try:
00365         filename_spec = abs_filename_spec(eval_text(filename_spec, symbols))
00366     except XacroException as e:
00367         if e.exc and isinstance(e.exc, NameError) and symbols is None:
00368             raise XacroException('variable filename is supported with --inorder option only')
00369         else:
00370             raise
00371 
00372     if re.search('[*[?]+', filename_spec):
00373         # Globbing behaviour
00374         filenames = sorted(glob.glob(filename_spec))
00375         if len(filenames) == 0:
00376             warning(include_no_matches_msg.format(filename_spec))
00377     else:
00378         # Default behaviour
00379         filenames = [filename_spec]
00380 
00381     for filename in filenames:
00382         global all_includes
00383         all_includes.append(filename)
00384         yield filename
00385 
00386 
00387 def import_xml_namespaces(parent, attributes):
00388     """import all namespace declarations into parent"""
00389     for name, value in attributes.items():
00390         if name.startswith('xmlns:'):
00391             oldAttr = parent.getAttributeNode(name)
00392             if oldAttr and oldAttr.value != value:
00393                 warning("inconsistent namespace redefinitions for {name}:"
00394                         "\n old: {old}\n new: {new} ({new_file})".format(
00395                     name=name, old=oldAttr.value, new=value,
00396                     new_file=filestack[-1]))
00397             else:
00398                 parent.setAttribute(name, value)
00399 
00400 
00401 def process_include(elt, macros, symbols, func):
00402     included = []
00403     filename_spec, namespace_spec = check_attrs(elt, ['filename'], ['ns'])
00404     if namespace_spec:
00405         try:
00406             namespace_spec = eval_text(namespace_spec, symbols)
00407             macros[namespace_spec] = ns_macros = MacroNameSpace()
00408             symbols[namespace_spec] = ns_symbols = PropertyNameSpace()
00409             macros = ns_macros
00410             symbols = ns_symbols
00411         except TypeError:
00412             raise XacroException('namespaces are supported with --inorder option only')
00413 
00414     for filename in get_include_files(filename_spec, symbols):
00415         # extend filestack
00416         oldstack = push_file(filename)
00417         include = parse(None, filename).documentElement
00418 
00419         # recursive call to func
00420         func(include, macros, symbols)
00421         included.append(include)
00422         import_xml_namespaces(elt.parentNode, include.attributes)
00423 
00424         # restore filestack
00425         restore_filestack(oldstack)
00426 
00427     remove_previous_comments(elt)
00428     # replace the include tag with the nodes of the included file(s)
00429     replace_node(elt, by=included, content_only=True)
00430 
00431 
00432 # @throws XacroException if a parsing error occurs with an included document
00433 def process_includes(elt, macros=None, symbols=None):
00434     elt = first_child_element(elt)
00435     while elt:
00436         next = next_sibling_element(elt)
00437         if is_include(elt):
00438             process_include(elt, macros, symbols, process_includes)
00439         else:
00440             process_includes(elt)
00441 
00442         elt = next
00443 
00444 
00445 def is_valid_name(name):
00446     """
00447     Checks whether name is a valid property or macro identifier.
00448     With python-based evaluation, we need to avoid name clashes with python keywords.
00449     """
00450     # Resulting AST of simple identifier is <Module [<Expr <Name "foo">>]>
00451     try:
00452         root = ast.parse(name)
00453 
00454         if isinstance(root, ast.Module) and \
00455            len(root.body) == 1 and isinstance(root.body[0], ast.Expr) and \
00456            isinstance(root.body[0].value, ast.Name) and root.body[0].value.id == name:
00457             return True
00458     except SyntaxError:
00459         pass
00460 
00461     return False
00462 
00463 
00464 re_macro_arg = re.compile(r'''\s*([^\s:=]+?):?=(\^\|?)?((?:(?:'[^']*')?[^\s'"]*?)*)(?:\s+|$)(.*)''')
00465 #                           space   param    :=   ^|   <--      default      -->   space    rest
00466 def parse_macro_arg(s):
00467     """
00468     parse the first param spec from a macro parameter string s
00469     accepting the following syntax: <param>[:=|=][^|]<default>
00470     :param s: param spec string
00471     :return: param, (forward, default), rest-of-string
00472              forward will be either param or None (depending on whether ^ was specified)
00473              default will be the default string or None
00474              If there is no default spec at all, the middle pair will be replaced by None
00475     """
00476     m = re_macro_arg.match(s)
00477     if m:
00478         # there is a default value specified for param
00479         param, forward, default, rest = m.groups()
00480         if not default: default = None
00481         return param, (param if forward else None, default), rest
00482     else:
00483         # there is no default specified at all
00484         result = s.lstrip(' ').split(' ', 1)
00485         return result[0], None, result[1] if len(result) > 1 else ''
00486 
00487 
00488 def grab_macro(elt, macros):
00489     assert(elt.tagName in ['macro', 'xacro:macro'])
00490     remove_previous_comments(elt)
00491 
00492     name, params = check_attrs(elt, ['name'], ['params'])
00493     if name == 'call':
00494         warning("deprecated use of macro name 'call'; xacro:call became a new keyword")
00495     if name.find('.') != -1:
00496         warning("macro names must not contain '.': %s" % name)
00497 
00498     # fetch existing or create new macro definition
00499     macro = macros.get(name, Macro())
00500     # append current filestack to history
00501     macro.history.append(filestack)
00502     macro.body = elt
00503 
00504     # parse params and their defaults
00505     macro.params = []
00506     macro.defaultmap = {}
00507     while params:
00508         param, value, params = parse_macro_arg(params)
00509         macro.params.append(param)
00510         if value is not None:
00511             macro.defaultmap[param] = value  # parameter with default
00512 
00513     macros[name] = macro
00514     replace_node(elt, by=None)
00515 
00516 
00517 # Fill the dictionary { macro_name => macro_xml_block }
00518 def grab_macros(elt, macros):
00519     elt = first_child_element(elt)
00520     while elt:
00521         next = next_sibling_element(elt)
00522         if elt.tagName in ['macro', 'xacro:macro'] \
00523                 and check_deprecated_tag(elt.tagName):
00524             grab_macro(elt, macros)
00525         else:
00526             grab_macros(elt, macros)
00527 
00528         elt = next
00529 
00530 
00531 def grab_property(elt, table):
00532     assert(elt.tagName in ['property', 'xacro:property'])
00533     remove_previous_comments(elt)
00534 
00535     name, value, scope = check_attrs(elt, ['name'], ['value', 'scope'])
00536     if not is_valid_name(name):
00537         raise XacroException('Property names must be valid python identifiers: ' + name)
00538 
00539     if value is None:
00540         name = '**' + name
00541         value = elt  # debug
00542 
00543     if scope and scope == 'global':
00544         target_table = table.root()
00545         unevaluated = False
00546     elif scope and scope == 'parent':
00547         if table.parent:
00548             target_table = table.parent
00549         else:
00550             warning("%s: no parent scope at global scope " % name)
00551         unevaluated = False
00552     else:
00553         target_table = table
00554         unevaluated = True
00555 
00556     if not unevaluated and isinstance(value, _basestr):
00557         value = eval_text(value, table)
00558 
00559     target_table._setitem(name, value, unevaluated=unevaluated)
00560     replace_node(elt, by=None)
00561 
00562 
00563 # Fill the table of the properties
00564 def grab_properties(elt, table):
00565     elt = first_child_element(elt)
00566     while elt:
00567         next = next_sibling_element(elt)
00568         if elt.tagName in ['property', 'xacro:property'] \
00569                 and check_deprecated_tag(elt.tagName):
00570             grab_property(elt, table)
00571         else:
00572             grab_properties(elt, table)
00573 
00574         elt = next
00575 
00576 
00577 LEXER = QuickLexer(DOLLAR_DOLLAR_BRACE=r"\$\$+\{",
00578                    EXPR=r"\$\{[^\}]*\}",
00579                    EXTENSION=r"\$\([^\)]*\)",
00580                    TEXT=r"([^\$]|\$[^{(]|\$$)+")
00581 
00582 
00583 # evaluate text and return typed value
00584 def eval_text(text, symbols):
00585     def handle_expr(s):
00586         try:
00587             return eval(eval_text(s, symbols), global_symbols, symbols)
00588         except Exception as e:
00589             # re-raise as XacroException to add more context
00590             raise XacroException(exc=e,
00591                 suffix=os.linesep + "when evaluating expression '%s'" % s)
00592 
00593     def handle_extension(s):
00594         return eval_extension("$(%s)" % eval_text(s, symbols))
00595 
00596     results = []
00597     lex = QuickLexer(LEXER)
00598     lex.lex(text)
00599     while lex.peek():
00600         if lex.peek()[0] == lex.EXPR:
00601             results.append(handle_expr(lex.next()[1][2:-1]))
00602         elif lex.peek()[0] == lex.EXTENSION:
00603             results.append(handle_extension(lex.next()[1][2:-1]))
00604         elif lex.peek()[0] == lex.TEXT:
00605             results.append(lex.next()[1])
00606         elif lex.peek()[0] == lex.DOLLAR_DOLLAR_BRACE:
00607             results.append(lex.next()[1][1:])
00608     # return single element as is, i.e. typed
00609     if len(results) == 1:
00610         return results[0]
00611     # otherwise join elements to a string
00612     else:
00613         return ''.join(map(str, results))
00614 
00615 
00616 def eval_default_arg(forward_variable, default, symbols, macro):
00617     if forward_variable is None:
00618         return eval_text(default, symbols)
00619     try:
00620         return symbols[forward_variable]
00621     except KeyError:
00622         if default is not None:
00623             return eval_text(default, symbols)
00624         else:
00625             raise XacroException("Undefined property to forward: " + forward_variable, macro=macro)
00626 
00627 
00628 def handle_dynamic_macro_call(node, macros, symbols):
00629     name, = reqd_attrs(node, ['macro'])
00630     if not name:
00631         raise XacroException("xacro:call is missing the 'macro' attribute")
00632     name = str(eval_text(name, symbols))
00633 
00634     # remove 'macro' attribute and rename tag with resolved macro name
00635     node.removeAttribute('macro')
00636     node.tagName = name
00637     # forward to handle_macro_call
00638     result = handle_macro_call(node, macros, symbols)
00639     if not result:  # we expect the call to succeed
00640         raise XacroException("unknown macro name '%s' in xacro:call" % name)
00641     return True
00642 
00643 def resolve_macro(fullname, macros):
00644     # split name into namespaces and real name
00645     namespaces = fullname.split('.')
00646     name = namespaces.pop(-1)
00647     # move xacro: prefix from first namespace to name
00648     if namespaces and namespaces[0].startswith('xacro:'):
00649         namespaces[0] = namespaces[0].replace('xacro:', '')
00650         name = 'xacro:' + name
00651 
00652     def _resolve(namespaces, name, macros):
00653         # traverse namespaces to actual macros dict
00654         for ns in namespaces:
00655             macros = macros[ns]
00656         try:
00657             return macros[name]
00658         except KeyError:
00659             # try without xacro: prefix as well
00660             if name.startswith('xacro:'):
00661                 return _resolve([], name.replace('xacro:',''), macros)
00662 
00663     # try fullname and (namespaces, name) in this order
00664     m = _resolve([], fullname, macros)
00665     if m: return m
00666     elif namespaces: return _resolve(namespaces, name, macros)\
00667 
00668 
00669 def handle_macro_call(node, macros, symbols):
00670     try:
00671         m = resolve_macro(node.tagName, macros)
00672         body = m.body.cloneNode(deep=True)
00673 
00674     except (KeyError, TypeError, AttributeError):
00675         # TODO If deprecation runs out, this test should be moved up front
00676         if node.tagName == 'xacro:call':
00677             return handle_dynamic_macro_call(node, macros, symbols)
00678         return False  # no macro
00679 
00680     # Expand the macro
00681     scoped = Table(symbols)  # new local name space for macro evaluation
00682     params = m.params[:]  # deep copy macro's params list
00683     for name, value in node.attributes.items():
00684         if name not in params:
00685             raise XacroException("Invalid parameter \"%s\"" % str(name), macro=m)
00686         params.remove(name)
00687         scoped._setitem(name, eval_text(value, symbols), unevaluated=False)
00688         node.setAttribute(name, "")  # suppress second evaluation in eval_all()
00689 
00690     # Evaluate block parameters in node
00691     eval_all(node, macros, symbols)
00692 
00693     # Fetch block parameters, in order
00694     block = first_child_element(node)
00695     for param in params[:]:
00696         if param[0] == '*':
00697             if not block:
00698                 raise XacroException("Not enough blocks", macro=m)
00699             params.remove(param)
00700             scoped[param] = block
00701             block = next_sibling_element(block)
00702 
00703     if block is not None:
00704         raise XacroException("Unused block \"%s\"" % block.tagName, macro=m)
00705 
00706     # Try to load defaults for any remaining non-block parameters
00707     for param in params[:]:
00708         # block parameters are not supported for defaults
00709         if param[0] == '*': continue
00710 
00711         # get default
00712         name, default = m.defaultmap.get(param, (None,None))
00713         if name is not None or default is not None:
00714             scoped._setitem(param, eval_default_arg(name, default, symbols, m), unevaluated=False)
00715             params.remove(param)
00716 
00717     if params:
00718         raise XacroException("Undefined parameters [%s]" % ",".join(params), macro=m)
00719 
00720     try:
00721         eval_all(body, macros, scoped)
00722     except Exception as e:
00723         if hasattr(e, 'macros'):
00724             e.macros.append(m)
00725         else:
00726             e.macros = [m]
00727         raise
00728 
00729     # Replaces the macro node with the expansion
00730     remove_previous_comments(node)
00731     replace_node(node, by=body, content_only=True)
00732     return True
00733 
00734 
00735 def get_boolean_value(value, condition):
00736     """
00737     Return a boolean value that corresponds to the given Xacro condition value.
00738     Values "true", "1" and "1.0" are supposed to be True.
00739     Values "false", "0" and "0.0" are supposed to be False.
00740     All other values raise an exception.
00741 
00742     :param value: The value to be evaluated. The value has to already be evaluated by Xacro.
00743     :param condition: The original condition text in the XML.
00744     :return: The corresponding boolean value, or a Python expression that, converted to boolean, corresponds to it.
00745     :raises ValueError: If the condition value is incorrect.
00746     """
00747     try:
00748         if isinstance(value, _basestr):
00749             if value == 'true': return True
00750             elif value == 'false': return False
00751             else: return ast.literal_eval(value)
00752         else:
00753             return bool(value)
00754     except:
00755         raise XacroException("Xacro conditional \"%s\" evaluated to \"%s\", "
00756                              "which is not a boolean expression." % (condition, value))
00757 
00758 
00759 _empty_text_node = xml.dom.minidom.getDOMImplementation().createDocument(None, "dummy", None).createTextNode('\n\n')
00760 def remove_previous_comments(node):
00761     """remove consecutive comments in front of the xacro-specific node"""
00762     next = node.nextSibling
00763     previous = node.previousSibling
00764     while previous:
00765         if previous.nodeType == xml.dom.Node.TEXT_NODE and \
00766                 previous.data.isspace() and previous.data.count('\n') <= 1:
00767             previous = previous.previousSibling  # skip a single empty text node (max 1 newline)
00768 
00769         if previous and previous.nodeType == xml.dom.Node.COMMENT_NODE:
00770             comment = previous
00771             previous = previous.previousSibling
00772             node.parentNode.removeChild(comment)
00773         else:
00774             # insert empty text node to stop removing of comments in future calls
00775             # actually this moves the singleton instance to the new location
00776             if next: node.parentNode.insertBefore(_empty_text_node, next)
00777             return
00778 
00779 
00780 def eval_all(node, macros, symbols):
00781     """Recursively evaluate node, expanding macros, replacing properties, and evaluating expressions"""
00782     # evaluate the attributes
00783     for name, value in node.attributes.items():
00784         result = str(eval_text(value, symbols))
00785         node.setAttribute(name, result)
00786 
00787     node = node.firstChild
00788     while node:
00789         next = node.nextSibling
00790         if node.nodeType == xml.dom.Node.ELEMENT_NODE:
00791             if node.tagName in ['insert_block', 'xacro:insert_block'] \
00792                     and check_deprecated_tag(node.tagName):
00793                 name, = check_attrs(node, ['name'], [])
00794 
00795                 if ("**" + name) in symbols:
00796                     # Multi-block
00797                     block = symbols['**' + name]
00798                     content_only = True
00799                 elif ("*" + name) in symbols:
00800                     # Single block
00801                     block = symbols['*' + name]
00802                     content_only = False
00803                 else:
00804                     raise XacroException("Undefined block \"%s\"" % name)
00805 
00806                 # cloning block allows to insert the same block multiple times
00807                 block = block.cloneNode(deep=True)
00808                 # recursively evaluate block
00809                 eval_all(block, macros, symbols)
00810                 replace_node(node, by=block, content_only=content_only)
00811 
00812             elif is_include(node):
00813                 process_include(node, macros, symbols, eval_all)
00814 
00815             elif node.tagName in ['property', 'xacro:property'] \
00816                     and check_deprecated_tag(node.tagName):
00817                 grab_property(node, symbols)
00818 
00819             elif node.tagName in ['macro', 'xacro:macro'] \
00820                     and check_deprecated_tag(node.tagName):
00821                 grab_macro(node, macros)
00822 
00823             elif node.tagName in ['arg', 'xacro:arg'] \
00824                     and check_deprecated_tag(node.tagName):
00825                 name, default = check_attrs(node, ['name', 'default'], [])
00826                 if name not in substitution_args_context['arg']:
00827                     substitution_args_context['arg'][name] = eval_text(default, symbols)
00828 
00829                 remove_previous_comments(node)
00830                 replace_node(node, by=None)
00831 
00832             elif node.tagName == 'xacro:element':
00833                 name = eval_text(*reqd_attrs(node, ['xacro:name']), symbols=symbols)
00834                 if not name:
00835                     raise XacroException("xacro:element: empty name")
00836 
00837                 node.removeAttribute('xacro:name')
00838                 node.nodeName = node.tagName = name
00839                 continue  # re-process the node with new tagName
00840 
00841             elif node.tagName == 'xacro:attribute':
00842                 name, value = [eval_text(a, symbols) for a in reqd_attrs(node, ['name', 'value'])]
00843                 if not name:
00844                     raise XacroException("xacro:attribute: empty name")
00845 
00846                 node.parentNode.setAttribute(name, value)
00847                 replace_node(node, by=None)
00848 
00849             elif node.tagName in ['if', 'xacro:if', 'unless', 'xacro:unless'] \
00850                     and check_deprecated_tag(node.tagName):
00851                 remove_previous_comments(node)
00852                 cond, = check_attrs(node, ['value'], [])
00853                 keep = get_boolean_value(eval_text(cond, symbols), cond)
00854                 if node.tagName in ['unless', 'xacro:unless']:
00855                     keep = not keep
00856 
00857                 if keep:
00858                     eval_all(node, macros, symbols)
00859                     replace_node(node, by=node, content_only=True)
00860                 else:
00861                     replace_node(node, by=None)
00862 
00863             elif handle_macro_call(node, macros, symbols):
00864                 pass  # handle_macro_call does all the work of expanding the macro
00865 
00866             else:
00867                 # these are the non-xacro tags
00868                 if node.tagName.startswith("xacro:"):
00869                     raise XacroException("unknown macro name: %s" % node.tagName)
00870 
00871                 eval_all(node, macros, symbols)
00872 
00873         # TODO: Also evaluate content of COMMENT_NODEs?
00874         elif node.nodeType == xml.dom.Node.TEXT_NODE:
00875             node.data = str(eval_text(node.data, symbols))
00876 
00877         node = next
00878 
00879 
00880 def parse(inp, filename=None):
00881     """
00882     Parse input or filename into a DOM tree.
00883     If inp is None, open filename and load from there.
00884     Otherwise, parse inp, either as string or file object.
00885     If inp is already a DOM tree, this function is a noop.
00886     :return:xml.dom.minidom.Document
00887     :raise: xml.parsers.expat.ExpatError
00888     """
00889     f = None
00890     if inp is None:
00891         try:
00892             inp = f = open(filename)
00893         except IOError as e:
00894             # do not report currently processed file as "in file ..."
00895             filestack.pop()
00896             raise XacroException(e.strerror + ": " + e.filename)
00897 
00898     try:
00899         if isinstance(inp, _basestr):
00900             return xml.dom.minidom.parseString(inp)
00901         elif isinstance(inp, file):
00902             return xml.dom.minidom.parse(inp)
00903         return inp
00904 
00905     finally:
00906         if f:
00907             f.close()
00908 
00909 
00910 def process_doc(doc,
00911                 in_order=False, just_deps=False, just_includes=False,
00912                 mappings=None, xacro_ns=True, **kwargs):
00913     global verbosity, do_check_order
00914     verbosity = kwargs.get('verbosity', verbosity)
00915     do_check_order = kwargs.get('do_check_order', do_check_order)
00916 
00917     # set substitution args
00918     if mappings is not None:
00919         substitution_args_context['arg'] = mappings
00920 
00921     global allow_non_prefixed_tags
00922     allow_non_prefixed_tags = xacro_ns
00923 
00924     # if not yet defined: initialize filestack
00925     if not filestack: restore_filestack([None])
00926 
00927     # inorder processing requires to process the whole document for deps too
00928     # because filenames might be specified via properties or macro parameters
00929     if (just_deps or just_includes) and not in_order:
00930         process_includes(doc.documentElement)
00931         return
00932 
00933     macros = {}
00934     symbols = Table()
00935     if not in_order:
00936         # process includes, macros, and properties before evaluating stuff
00937         process_includes(doc.documentElement)
00938         grab_macros(doc, macros)
00939         grab_properties(doc, symbols)
00940 
00941     eval_all(doc.documentElement, macros, symbols)
00942 
00943     # reset substitution args
00944     substitution_args_context['arg'] = {}
00945 
00946     if do_check_order and symbols.redefined:
00947         warning("Document is incompatible to --inorder processing.")
00948         warning("The following properties were redefined after usage:")
00949         for k, v in symbols.redefined.iteritems():
00950             message(k, "redefined in", v, color='yellow')
00951 
00952 
00953 def open_output(output_filename):
00954     if output_filename is None:
00955         return sys.stdout
00956     else:
00957         dir_name = os.path.dirname(output_filename)
00958         if dir_name:
00959             try:
00960                 os.makedirs(dir_name)
00961             except os.error:
00962                 # errors occur when dir_name exists or creation failed
00963                 # ignore error here; opening of file will fail if directory is still missing
00964                 pass
00965 
00966         try:
00967             return open(output_filename, 'w')
00968         except IOError as e:
00969             raise XacroException("Failed to open output:", exc=e)
00970 
00971 
00972 def print_location(filestack, err=None, file=sys.stderr):
00973     macros = getattr(err, 'macros', []) if err else []
00974     msg = 'when instantiating macro:'
00975     for m in macros:
00976         name = m.body.getAttribute('name')
00977         location = '(%s)' % m.history[-1][-1]
00978         print(msg, name, location, file=file)
00979         msg = 'instantiated from:'
00980 
00981     msg = 'in file:' if macros else 'when processing file:'
00982     for f in reversed(filestack):
00983         if f is None: f = 'string'
00984         print(msg, f, file=file)
00985         msg = 'included from:'
00986 
00987 
00988 def main():
00989     opts, input_file = process_args(sys.argv[1:])
00990     if opts.in_order == False:
00991         warning("Traditional processing is deprecated. Switch to --inorder processing!")
00992         message("To check for compatibility of your document, use option --check-order.", color='yellow')
00993         message("For more infos, see http://wiki.ros.org/xacro#Processing_Order", color='yellow')
00994 
00995     try:
00996         restore_filestack([input_file])
00997         doc = parse(None, input_file)
00998         process_doc(doc, **vars(opts))
00999         out = open_output(opts.output)
01000 
01001     except xml.parsers.expat.ExpatError as e:
01002         error("XML parsing error: %s" % str(e), alt_text=None)
01003         if verbosity > 0:
01004             print_location(filestack, e)
01005             print(file=sys.stderr) # add empty separator line before error
01006             print("Check that:", file=sys.stderr)
01007             print(" - Your XML is well-formed", file=sys.stderr)
01008             print(" - You have the xacro xmlns declaration:",
01009                   "xmlns:xacro=\"http://www.ros.org/wiki/xacro\"", file=sys.stderr)
01010         sys.exit(2)  # indicate failure, but don't print stack trace on XML errors
01011 
01012     except Exception as e:
01013         error(str(e))
01014         if verbosity > 0:
01015             print_location(filestack, e)
01016         if verbosity > 1:
01017             print(file=sys.stderr)  # add empty separator line before error
01018             raise  # create stack trace
01019         else:
01020             sys.exit(2)  # gracefully exit with error condition
01021 
01022     if opts.just_deps:
01023         out.write(" ".join(set(all_includes)))
01024         print()
01025         return
01026     if opts.just_includes:
01027         out.write(doc.toprettyxml(indent='  '))
01028         print()
01029         return
01030 
01031     banner = [xml.dom.minidom.Comment(c) for c in
01032               [" %s " % ('=' * 83),
01033                " |    This document was autogenerated by xacro from %-30s | " % input_file,
01034                " |    EDITING THIS FILE BY HAND IS NOT RECOMMENDED  %-30s | " % "",
01035                " %s " % ('=' * 83)]]
01036     first = doc.firstChild
01037     for comment in banner:
01038         doc.insertBefore(comment, first)
01039 
01040     out.write(doc.toprettyxml(indent='  '))
01041     print()
01042     if opts.output:
01043         out.close()


xacro
Author(s): Stuart Glaser, William Woodall
autogenerated on Sat Jun 8 2019 18:50:42