src/xacro/__init__.py
Go to the documentation of this file.
1 # Copyright (c) 2015, Open Source Robotics Foundation, Inc.
2 # Copyright (c) 2013, Willow Garage, Inc.
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above copyright
11 # notice, this list of conditions and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
13 # * Neither the name of the Open Source Robotics Foundation, Inc.
14 # nor the names of its contributors may be used to endorse or promote
15 # products derived from this software without specific prior
16 # written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 # POSSIBILITY OF SUCH DAMAGE.
29 
30 # Authors: Stuart Glaser, William Woodall, Robert Haschke
31 # Maintainer: Morgan Quigley <morgan@osrfoundation.org>
32 
33 from __future__ import print_function, division
34 
35 import glob
36 import os
37 import re
38 import sys
39 import ast
40 import math
41 
42 from copy import deepcopy
43 from .color import warning, error, message
44 from .xmlutils import *
45 from .cli import process_args
46 
47 
48 try: # python 2
49  _basestr = basestring
50  encoding = { 'encoding': 'utf-8' }
51 except NameError: # python 3
52  _basestr = str
53  unicode = str
54  encoding = {}
55 
56 # Dictionary of substitution args
57 substitution_args_context = {}
58 
59 
60 # Stack of currently processed files
61 filestack = []
62 
63 def push_file(filename):
64  """
65  Push a new filename to the filestack.
66  Instead of directly modifying filestack, a deep-copy is created and modified,
67  while the old filestack is returned.
68  This allows to store the filestack that was active when a macro or property is defined
69  """
70  global filestack
71  oldstack = filestack
72  filestack = deepcopy(filestack)
73  filestack.append(filename)
74  return oldstack
75 
76 def restore_filestack(oldstack):
77  global filestack
78  filestack = oldstack
79 
80 
81 def abs_filename_spec(filename_spec):
82  """
83  Prepend the dirname of the currently processed file
84  if filename_spec is not yet absolute
85  """
86  if not os.path.isabs(filename_spec):
87  parent_filename = filestack[-1]
88  basedir = os.path.dirname(parent_filename) if parent_filename else '.'
89  return os.path.join(basedir, filename_spec)
90  return filename_spec
91 
92 
93 def load_yaml(filename):
94  try:
95  import yaml
96  except:
97  raise XacroException("yaml support not available; install python-yaml")
98 
99  filename = abs_filename_spec(filename)
100  f = open(filename)
101  oldstack = push_file(filename)
102  try:
103  return yaml.load(f)
104  finally:
105  f.close()
106  restore_filestack(oldstack)
107  global all_includes
108  all_includes.append(filename)
109 
110 
111 # global symbols dictionary
112 # taking simple security measures to forbid access to __builtins__
113 # only the very few symbols explicitly listed are allowed
114 # for discussion, see: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
115 global_symbols = {'__builtins__': {k: __builtins__[k] for k in ['list', 'dict', 'map', 'str', 'float', 'int', 'True', 'False', 'min', 'max', 'round']}}
116 # also define all math symbols and functions
117 global_symbols.update(math.__dict__)
118 # allow to import dicts from yaml
119 global_symbols.update(dict(load_yaml=load_yaml))
120 
121 
122 class XacroException(Exception):
123  """
124  XacroException allows to wrap another exception (exc) and to augment
125  its error message: prefixing with msg and suffixing with suffix.
126  str(e) finally prints: msg str(exc) suffix
127  """
128  def __init__(self, msg=None, suffix=None, exc=None, macro=None):
129  super(XacroException, self).__init__(msg)
130  self.suffix = suffix
131  self.exc = exc
132  self.macros = [] if macro is None else [macro]
133 
134  def __str__(self):
135  items = [super(XacroException, self).__str__(), self.exc, self.suffix]
136  return ' '.join([unicode(e) for e in items if e not in ['', 'None']])
137 
138 
139 verbosity = 1
140 # deprecate non-namespaced use of xacro tags (issues #41, #59, #60)
141 def deprecated_tag(_issued=[False]):
142  if _issued[0]:
143  return
144  _issued[0] = True
145 
146  if verbosity > 0:
147  warning("deprecated: xacro tags should be prepended with 'xacro' xml namespace.")
148  message("""Use the following script to fix incorrect usage:
149  find . -iname "*.xacro" | xargs sed -i 's#<\([/]\\?\)\(if\|unless\|include\|arg\|property\|macro\|insert_block\)#<\\1xacro:\\2#g'""")
150  print_location(filestack)
151  print(file=sys.stderr)
152 
153 
154 # require xacro namespace?
155 allow_non_prefixed_tags = True
156 
157 
158 def check_deprecated_tag(tag_name):
159  """
160  Check whether tagName starts with xacro prefix. If not, issue a warning.
161  :param tag_name:
162  :return: True if tagName is accepted as xacro tag
163  False if tagName doesn't start with xacro prefix, but the prefix is required
164  """
165  if tag_name.startswith('xacro:'):
166  return True
167  else:
168  if allow_non_prefixed_tags:
170  return allow_non_prefixed_tags
171 
172 
173 class Macro(object):
174  def __init__(self):
175  self.body = None # original xml.dom.Node
176  self.params = [] # parsed parameter names
177  self.defaultmap = {} # default parameter values
178  self.history = [] # definition history
179 
180 
182  if s == '$(cwd)':
183  return os.getcwd()
184  try:
185  from roslaunch import substitution_args
186  from rospkg.common import ResourceNotFound
187  return substitution_args.resolve_args(s, context=substitution_args_context, resolve_anon=False)
188  except ImportError as e:
189  raise XacroException("substitution args not supported: ", exc=e)
190  except substitution_args.ArgException as e:
191  raise XacroException("Undefined substitution argument", exc=e)
192  except ResourceNotFound as e:
193  raise XacroException("resource not found:", exc=e)
194 
195 
196 do_check_order=False
197 class Table(object):
198  def __init__(self, parent=None):
199  self.parent = parent
200  self.table = {}
201  self.unevaluated = set() # set of unevaluated variables
202  self.recursive = [] # list of currently resolved vars (to resolve recursive definitions)
203  # the following variables are for debugging / checking only
204  self.depth = self.parent.depth + 1 if self.parent else 0
205  if do_check_order:
206  # this is for smooth transition from deprecated to --inorder processing
207  self.used = set() # set of used properties
208  self.redefined = dict() # set of properties redefined after usage
209 
210  @staticmethod
211  def _eval_literal(value):
212  if isinstance(value, _basestr):
213  # remove single quotes from escaped string
214  if len(value) >= 2 and value[0] == "'" and value[-1] == "'":
215  return value[1:-1]
216  # try to evaluate as number literal or boolean
217  # this is needed to handle numbers in property definitions as numbers, not strings
218  for f in [int, float, lambda x: get_boolean_value(x, None)]: # order of types is important!
219  try:
220  return f(value)
221  except:
222  pass
223  return value
224 
225  def _resolve_(self, key):
226  # lazy evaluation
227  if key in self.unevaluated:
228  if key in self.recursive:
229  raise XacroException("recursive variable definition: %s" %
230  " -> ".join(self.recursive + [key]))
231  self.recursive.append(key)
232  self.table[key] = self._eval_literal(eval_text(self.table[key], self))
233  self.unevaluated.remove(key)
234  self.recursive.remove(key)
235 
236  # return evaluated result
237  value = self.table[key]
238  if (verbosity > 2 and self.parent is None) or verbosity > 3:
239  print("{indent}use {key}: {value} ({loc})".format(
240  indent=self.depth*' ', key=key, value=value, loc=filestack[-1]), file=sys.stderr)
241  if do_check_order:
242  self.used.add(key)
243  return value
244 
245  def __getitem__(self, key):
246  if key in self.table:
247  return self._resolve_(key)
248  elif self.parent:
249  return self.parent[key]
250  else:
251  raise KeyError(key)
252 
253  def _setitem(self, key, value, unevaluated):
254  if do_check_order and key in self.used and key not in self.redefined:
255  self.redefined[key] = filestack[-1]
256 
257  if key in global_symbols:
258  warning("redefining global property: %s" % key)
259  print_location(filestack)
260 
261  value = self._eval_literal(value)
262  self.table[key] = value
263  if unevaluated and isinstance(value, _basestr):
264  # literal evaluation failed: re-evaluate lazily at first access
265  self.unevaluated.add(key)
266  elif key in self.unevaluated:
267  # all other types cannot be evaluated
268  self.unevaluated.remove(key)
269  if (verbosity > 2 and self.parent is None) or verbosity > 3:
270  print("{indent}set {key}: {value} ({loc})".format(
271  indent=self.depth*' ', key=key, value=value, loc=filestack[-1]), file=sys.stderr)
272 
273  def __setitem__(self, key, value):
274  self._setitem(key, value, unevaluated=True)
275 
276  def __contains__(self, key):
277  return \
278  key in self.table or \
279  (self.parent and key in self.parent)
280 
281  def __str__(self):
282  s = unicode(self.table)
283  if isinstance(self.parent, Table):
284  s += "\n parent: "
285  s += unicode(self.parent)
286  return s
287 
288  def root(self):
289  p = self
290  while p.parent:
291  p = p.parent
292  return p
293 
294 class NameSpace(object):
295  # dot access (namespace.property) is forwarded to getitem()
296  def __getattr__(self, item):
297  return self.__getitem__(item)
298 
300  def __init__(self, parent=None):
301  super(PropertyNameSpace, self).__init__(parent)
302 
304  def __init__(self, *args, **kwargs):
305  super(MacroNameSpace, self).__init__(*args, **kwargs)
306 
307 
308 class QuickLexer(object):
309  def __init__(self, *args, **kwargs):
310  if args:
311  # copy attributes + variables from other instance
312  other = args[0]
313  self.__dict__.update(other.__dict__)
314  else:
315  self.res = []
316  for k, v in kwargs.items():
317  self.__setattr__(k, len(self.res))
318  self.res.append(re.compile(v))
319  self.str = ""
320  self.top = None
321 
322  def lex(self, str):
323  self.str = str
324  self.top = None
325  self.next()
326 
327  def peek(self):
328  return self.top
329 
330  def next(self):
331  result = self.top
332  self.top = None
333  for i in range(len(self.res)):
334  m = self.res[i].match(self.str)
335  if m:
336  self.top = (i, m.group(0))
337  self.str = self.str[m.end():]
338  break
339  return result
340 
341 
342 all_includes = []
343 
344 include_no_matches_msg = """Include tag's filename spec \"{}\" matched no files."""
345 
346 
347 def is_include(elt):
348  # Xacro should not use plain 'include' tags but only namespaced ones. Causes conflicts with
349  # other XML elements including Gazebo's <gazebo> extensions
350  if elt.tagName not in ['xacro:include', 'include']:
351  return False
352 
353  # Temporary fix for ROS Hydro and the xacro include scope problem
354  if elt.tagName == 'include':
355  # check if there is any element within the <include> tag. mostly we are concerned
356  # with Gazebo's <uri> element, but it could be anything. also, make sure the child
357  # nodes aren't just a single Text node, which is still considered a deprecated
358  # instance
359  if elt.childNodes and not (len(elt.childNodes) == 1 and
360  elt.childNodes[0].nodeType == elt.TEXT_NODE):
361  # this is not intended to be a xacro element, so we can ignore it
362  return False
363  else:
364  # throw a deprecated warning
365  return check_deprecated_tag(elt.tagName)
366  return True
367 
368 
369 def get_include_files(filename_spec, symbols):
370  try:
371  filename_spec = abs_filename_spec(eval_text(filename_spec, symbols))
372  except XacroException as e:
373  if e.exc and isinstance(e.exc, NameError) and symbols is None:
374  raise XacroException('variable filename is supported with --inorder option only')
375  else:
376  raise
377 
378  if re.search('[*[?]+', filename_spec):
379  # Globbing behaviour
380  filenames = sorted(glob.glob(filename_spec))
381  if len(filenames) == 0:
382  warning(include_no_matches_msg.format(filename_spec))
383  else:
384  # Default behaviour
385  filenames = [filename_spec]
386 
387  for filename in filenames:
388  global all_includes
389  all_includes.append(filename)
390  yield filename
391 
392 
393 def import_xml_namespaces(parent, attributes):
394  """import all namespace declarations into parent"""
395  for name, value in attributes.items():
396  if name.startswith('xmlns:'):
397  oldAttr = parent.getAttributeNode(name)
398  if oldAttr and oldAttr.value != value:
399  warning("inconsistent namespace redefinitions for {name}:"
400  "\n old: {old}\n new: {new} ({new_file})".format(
401  name=name, old=oldAttr.value, new=value,
402  new_file=filestack[-1]))
403  else:
404  parent.setAttribute(name, value)
405 
406 
407 def process_include(elt, macros, symbols, func):
408  included = []
409  filename_spec, namespace_spec = check_attrs(elt, ['filename'], ['ns'])
410  if namespace_spec:
411  try:
412  namespace_spec = eval_text(namespace_spec, symbols)
413  macros[namespace_spec] = ns_macros = MacroNameSpace()
414  symbols[namespace_spec] = ns_symbols = PropertyNameSpace()
415  macros = ns_macros
416  symbols = ns_symbols
417  except TypeError:
418  raise XacroException('namespaces are supported with --inorder option only')
419 
420  for filename in get_include_files(filename_spec, symbols):
421  # extend filestack
422  oldstack = push_file(filename)
423  include = parse(None, filename).documentElement
424 
425  # recursive call to func
426  func(include, macros, symbols)
427  included.append(include)
428  import_xml_namespaces(elt.parentNode, include.attributes)
429 
430  # restore filestack
431  restore_filestack(oldstack)
432 
433  remove_previous_comments(elt)
434  # replace the include tag with the nodes of the included file(s)
435  replace_node(elt, by=included, content_only=True)
436 
437 
438 # @throws XacroException if a parsing error occurs with an included document
439 def process_includes(elt, macros=None, symbols=None):
440  elt = first_child_element(elt)
441  while elt:
442  next = next_sibling_element(elt)
443  if is_include(elt):
444  process_include(elt, macros, symbols, process_includes)
445  else:
446  process_includes(elt)
447 
448  elt = next
449 
450 
451 def is_valid_name(name):
452  """
453  Checks whether name is a valid property or macro identifier.
454  With python-based evaluation, we need to avoid name clashes with python keywords.
455  """
456  # Resulting AST of simple identifier is <Module [<Expr <Name "foo">>]>
457  try:
458  root = ast.parse(name)
459 
460  if isinstance(root, ast.Module) and \
461  len(root.body) == 1 and isinstance(root.body[0], ast.Expr) and \
462  isinstance(root.body[0].value, ast.Name) and root.body[0].value.id == name:
463  return True
464  except SyntaxError:
465  pass
466 
467  return False
468 
469 
470 re_macro_arg = re.compile(r'''\s*([^\s:=]+?):?=(\^\|?)?((?:(?:'[^']*')?[^\s'"]*?)*)(?:\s+|$)(.*)''')
471 # space param := ^| <-- default --> space rest
473  """
474  parse the first param spec from a macro parameter string s
475  accepting the following syntax: <param>[:=|=][^|]<default>
476  :param s: param spec string
477  :return: param, (forward, default), rest-of-string
478  forward will be either param or None (depending on whether ^ was specified)
479  default will be the default string or None
480  If there is no default spec at all, the middle pair will be replaced by None
481  """
482  m = re_macro_arg.match(s)
483  if m:
484  # there is a default value specified for param
485  param, forward, default, rest = m.groups()
486  if not default: default = None
487  return param, (param if forward else None, default), rest
488  else:
489  # there is no default specified at all
490  result = s.lstrip().split(None, 1)
491  return result[0], None, result[1] if len(result) > 1 else ''
492 
493 
494 def grab_macro(elt, macros):
495  assert(elt.tagName in ['macro', 'xacro:macro'])
496  remove_previous_comments(elt)
497 
498  name, params = check_attrs(elt, ['name'], ['params'])
499  if name == 'call':
500  warning("deprecated use of macro name 'call'; xacro:call became a new keyword")
501  if name.find('.') != -1:
502  warning("macro names must not contain '.': %s" % name)
503  # always have 'xacro:' namespace in macro name
504  if not name.startswith('xacro:'):
505  name = 'xacro:' + name
506 
507  # fetch existing or create new macro definition
508  macro = macros.get(name, Macro())
509  # append current filestack to history
510  macro.history.append(filestack)
511  macro.body = elt
512 
513  # parse params and their defaults
514  macro.params = []
515  macro.defaultmap = {}
516  while params:
517  param, value, params = parse_macro_arg(params)
518  macro.params.append(param)
519  if value is not None:
520  macro.defaultmap[param] = value # parameter with default
521 
522  macros[name] = macro
523  replace_node(elt, by=None)
524 
525 
526 # Fill the dictionary { macro_name => macro_xml_block }
527 def grab_macros(elt, macros):
528  elt = first_child_element(elt)
529  while elt:
530  next = next_sibling_element(elt)
531  if elt.tagName in ['macro', 'xacro:macro'] \
532  and check_deprecated_tag(elt.tagName):
533  grab_macro(elt, macros)
534  else:
535  grab_macros(elt, macros)
536 
537  elt = next
538 
539 
540 def grab_property(elt, table):
541  assert(elt.tagName in ['property', 'xacro:property'])
542  remove_previous_comments(elt)
543 
544  name, value, default, scope = check_attrs(elt, ['name'], ['value', 'default', 'scope'])
545  if not is_valid_name(name):
546  raise XacroException('Property names must be valid python identifiers: ' + name)
547  if value is not None and default is not None:
548  raise XacroException('Property cannot define both a default and a value: ' + name)
549 
550  if default is not None:
551  if scope is not None:
552  warning("%s: default property value can only be defined on local scope" % name)
553  if name not in table:
554  value = default
555  else:
556  replace_node(elt, by=None)
557  return
558 
559  if value is None:
560  name = '**' + name
561  value = elt # debug
562 
563  replace_node(elt, by=None)
564 
565  if scope and scope == 'global':
566  target_table = table.root()
567  unevaluated = False
568  elif scope and scope == 'parent':
569  if table.parent:
570  target_table = table.parent
571  unevaluated = False
572  else:
573  warning("%s: no parent scope at global scope " % name)
574  return # cannot store the value, no reason to evaluate it
575  else:
576  target_table = table
577  unevaluated = True
578 
579  if not unevaluated and isinstance(value, _basestr):
580  value = eval_text(value, table)
581 
582  target_table._setitem(name, value, unevaluated=unevaluated)
583 
584 
585 # Fill the table of the properties
586 def grab_properties(elt, table):
587  elt = first_child_element(elt)
588  while elt:
589  next = next_sibling_element(elt)
590  if elt.tagName in ['property', 'xacro:property'] \
591  and check_deprecated_tag(elt.tagName):
592  if "default" in elt.attributes.keys():
593  raise XacroException('default property value supported with --inorder option only')
594  grab_property(elt, table)
595  else:
596  grab_properties(elt, table)
597 
598  elt = next
599 
600 
601 LEXER = QuickLexer(DOLLAR_DOLLAR_BRACE=r"^\$\$+(\{|\()", # multiple $ in a row, followed by { or (
602  EXPR=r"^\$\{[^\}]*\}", # stuff starting with ${
603  EXTENSION=r"^\$\([^\)]*\)", # stuff starting with $(
604  TEXT=r"[^$]+|\$[^{($]+|\$$") # any text w/o $ or $ following any chars except {($ or single $
605 
606 
607 # evaluate text and return typed value
608 def eval_text(text, symbols):
609  def handle_expr(s):
610  try:
611  return eval(eval_text(s, symbols), global_symbols, symbols)
612  except Exception as e:
613  # re-raise as XacroException to add more context
614  raise XacroException(exc=e,
615  suffix=os.linesep + "when evaluating expression '%s'" % s)
616 
617  def handle_extension(s):
618  return eval_extension("$(%s)" % eval_text(s, symbols))
619 
620  results = []
621  lex = QuickLexer(LEXER)
622  lex.lex(text)
623  while lex.peek():
624  id = lex.peek()[0]
625  if id == lex.EXPR:
626  results.append(handle_expr(lex.next()[1][2:-1]))
627  elif id == lex.EXTENSION:
628  results.append(handle_extension(lex.next()[1][2:-1]))
629  elif id == lex.TEXT:
630  results.append(lex.next()[1])
631  elif id == lex.DOLLAR_DOLLAR_BRACE:
632  results.append(lex.next()[1][1:])
633  # return single element as is, i.e. typed
634  if len(results) == 1:
635  return results[0]
636  # otherwise join elements to a string
637  else:
638  return ''.join(map(unicode, results))
639 
640 
641 def eval_default_arg(forward_variable, default, symbols, macro):
642  if forward_variable is None:
643  return eval_text(default, symbols)
644  try:
645  return symbols[forward_variable]
646  except KeyError:
647  if default is not None:
648  return eval_text(default, symbols)
649  else:
650  raise XacroException("Undefined property to forward: " + forward_variable, macro=macro)
651 
652 
653 def handle_dynamic_macro_call(node, macros, symbols):
654  name, = reqd_attrs(node, ['macro'])
655  if not name:
656  raise XacroException("xacro:call is missing the 'macro' attribute")
657  name = unicode(eval_text(name, symbols))
658 
659  # remove 'macro' attribute and rename tag with resolved macro name
660  node.removeAttribute('macro')
661  node.tagName = name
662  # forward to handle_macro_call
663  result = handle_macro_call(node, macros, symbols)
664  if not result: # we expect the call to succeed
665  raise XacroException("unknown macro name '%s' in xacro:call" % name)
666  return True
667 
668 def resolve_macro(fullname, macros):
669  # split name into namespaces and real name
670  namespaces = fullname.split('.')
671  name = namespaces.pop(-1)
672  # move xacro: prefix from first namespace to name
673  if namespaces and namespaces[0].startswith('xacro:'):
674  namespaces[0] = namespaces[0].replace('xacro:', '')
675  name = 'xacro:' + name
676 
677  def _resolve(namespaces, name, macros):
678  # traverse namespaces to actual macros dict
679  for ns in namespaces:
680  macros = macros[ns]
681  try:
682  return macros[name]
683  except KeyError:
684  # try with xacro: prefix as well
685  if allow_non_prefixed_tags and not name.startswith('xacro:'):
686  return _resolve([], 'xacro:' + name, macros)
687 
688  # try fullname and (namespaces, name) in this order
689  m = _resolve([], fullname, macros)
690  if m: return m
691  elif namespaces: return _resolve(namespaces, name, macros)\
692 
693 
694 def handle_macro_call(node, macros, symbols):
695  try:
696  m = resolve_macro(node.tagName, macros)
697  body = m.body.cloneNode(deep=True)
698 
699  except (KeyError, TypeError, AttributeError):
700  # TODO If deprecation runs out, this test should be moved up front
701  if node.tagName == 'xacro:call':
702  return handle_dynamic_macro_call(node, macros, symbols)
703  return False # no macro
704 
705  # Expand the macro
706  scoped = Table(symbols) # new local name space for macro evaluation
707  params = m.params[:] # deep copy macro's params list
708  for name, value in node.attributes.items():
709  if name not in params:
710  raise XacroException("Invalid parameter \"%s\"" % unicode(name), macro=m)
711  params.remove(name)
712  scoped._setitem(name, eval_text(value, symbols), unevaluated=False)
713  node.setAttribute(name, "") # suppress second evaluation in eval_all()
714 
715  # Evaluate block parameters in node
716  eval_all(node, macros, symbols)
717 
718  # Fetch block parameters, in order
719  block = first_child_element(node)
720  for param in params[:]:
721  if param[0] == '*':
722  if not block:
723  raise XacroException("Not enough blocks", macro=m)
724  params.remove(param)
725  scoped[param] = block
726  block = next_sibling_element(block)
727 
728  if block is not None:
729  raise XacroException("Unused block \"%s\"" % block.tagName, macro=m)
730 
731  # Try to load defaults for any remaining non-block parameters
732  for param in params[:]:
733  # block parameters are not supported for defaults
734  if param[0] == '*': continue
735 
736  # get default
737  name, default = m.defaultmap.get(param, (None,None))
738  if name is not None or default is not None:
739  scoped._setitem(param, eval_default_arg(name, default, symbols, m), unevaluated=False)
740  params.remove(param)
741 
742  if params:
743  raise XacroException("Undefined parameters [%s]" % ",".join(params), macro=m)
744 
745  try:
746  eval_all(body, macros, scoped)
747  except Exception as e:
748  # fill in macro call history for nice error reporting
749  if hasattr(e, 'macros'):
750  e.macros.append(m)
751  else:
752  e.macros = [m]
753  raise
754 
755  # Replaces the macro node with the expansion
756  remove_previous_comments(node)
757  replace_node(node, by=body, content_only=True)
758  return True
759 
760 
761 def get_boolean_value(value, condition):
762  """
763  Return a boolean value that corresponds to the given Xacro condition value.
764  Values "true", "1" and "1.0" are supposed to be True.
765  Values "false", "0" and "0.0" are supposed to be False.
766  All other values raise an exception.
767 
768  :param value: The value to be evaluated. The value has to already be evaluated by Xacro.
769  :param condition: The original condition text in the XML.
770  :return: The corresponding boolean value, or a Python expression that, converted to boolean, corresponds to it.
771  :raises ValueError: If the condition value is incorrect.
772  """
773  try:
774  if isinstance(value, _basestr):
775  if value == 'true' or value == 'True': return True
776  elif value == 'false' or value == 'False': return False
777  else: return bool(int(value))
778  else:
779  return bool(value)
780  except:
781  raise XacroException("Xacro conditional \"%s\" evaluated to \"%s\", "
782  "which is not a boolean expression." % (condition, value))
783 
784 
785 _empty_text_node = xml.dom.minidom.getDOMImplementation().createDocument(None, "dummy", None).createTextNode('\n\n')
786 def remove_previous_comments(node):
787  """remove consecutive comments in front of the xacro-specific node"""
788  next = node.nextSibling
789  previous = node.previousSibling
790  while previous:
791  if previous.nodeType == xml.dom.Node.TEXT_NODE and \
792  previous.data.isspace() and previous.data.count('\n') <= 1:
793  previous = previous.previousSibling # skip a single empty text node (max 1 newline)
794 
795  if previous and previous.nodeType == xml.dom.Node.COMMENT_NODE:
796  comment = previous
797  previous = previous.previousSibling
798  node.parentNode.removeChild(comment)
799  else:
800  # insert empty text node to stop removing of comments in future calls
801  # actually this moves the singleton instance to the new location
802  if next and _empty_text_node != next: node.parentNode.insertBefore(_empty_text_node, next)
803  return
804 
805 
806 def eval_all(node, macros, symbols):
807  """Recursively evaluate node, expanding macros, replacing properties, and evaluating expressions"""
808  # evaluate the attributes
809  for name, value in node.attributes.items():
810  result = unicode(eval_text(value, symbols))
811  node.setAttribute(name, result)
812 
813  node = node.firstChild
814  while node:
815  next = node.nextSibling
816  if node.nodeType == xml.dom.Node.ELEMENT_NODE:
817  if node.tagName in ['insert_block', 'xacro:insert_block'] \
818  and check_deprecated_tag(node.tagName):
819  name, = check_attrs(node, ['name'], [])
820 
821  if ("**" + name) in symbols:
822  # Multi-block
823  block = symbols['**' + name]
824  content_only = True
825  elif ("*" + name) in symbols:
826  # Single block
827  block = symbols['*' + name]
828  content_only = False
829  else:
830  raise XacroException("Undefined block \"%s\"" % name)
831 
832  # cloning block allows to insert the same block multiple times
833  block = block.cloneNode(deep=True)
834  # recursively evaluate block
835  eval_all(block, macros, symbols)
836  replace_node(node, by=block, content_only=content_only)
837 
838  elif is_include(node):
839  process_include(node, macros, symbols, eval_all)
840 
841  elif node.tagName in ['property', 'xacro:property'] \
842  and check_deprecated_tag(node.tagName):
843  grab_property(node, symbols)
844 
845  elif node.tagName in ['macro', 'xacro:macro'] \
846  and check_deprecated_tag(node.tagName):
847  grab_macro(node, macros)
848 
849  elif node.tagName in ['arg', 'xacro:arg'] \
850  and check_deprecated_tag(node.tagName):
851  name, default = check_attrs(node, ['name', 'default'], [])
852  if name not in substitution_args_context['arg']:
853  substitution_args_context['arg'][name] = eval_text(default, symbols)
854 
855  remove_previous_comments(node)
856  replace_node(node, by=None)
857 
858  elif node.tagName == 'xacro:element':
859  name = eval_text(*reqd_attrs(node, ['xacro:name']), symbols=symbols)
860  if not name:
861  raise XacroException("xacro:element: empty name")
862 
863  node.removeAttribute('xacro:name')
864  node.nodeName = node.tagName = name
865  continue # re-process the node with new tagName
866 
867  elif node.tagName == 'xacro:attribute':
868  name, value = [eval_text(a, symbols) for a in reqd_attrs(node, ['name', 'value'])]
869  if not name:
870  raise XacroException("xacro:attribute: empty name")
871 
872  node.parentNode.setAttribute(name, value)
873  replace_node(node, by=None)
874 
875  elif node.tagName in ['if', 'xacro:if', 'unless', 'xacro:unless'] \
876  and check_deprecated_tag(node.tagName):
877  remove_previous_comments(node)
878  cond, = check_attrs(node, ['value'], [])
879  keep = get_boolean_value(eval_text(cond, symbols), cond)
880  if node.tagName in ['unless', 'xacro:unless']:
881  keep = not keep
882 
883  if keep:
884  eval_all(node, macros, symbols)
885  replace_node(node, by=node, content_only=True)
886  else:
887  replace_node(node, by=None)
888 
889  elif handle_macro_call(node, macros, symbols):
890  pass # handle_macro_call does all the work of expanding the macro
891 
892  else:
893  # these are the non-xacro tags
894  if node.tagName.startswith("xacro:"):
895  raise XacroException("unknown macro name: %s" % node.tagName)
896 
897  eval_all(node, macros, symbols)
898 
899  # TODO: Also evaluate content of COMMENT_NODEs?
900  elif node.nodeType == xml.dom.Node.TEXT_NODE:
901  node.data = unicode(eval_text(node.data, symbols))
902 
903  node = next
904 
905 
906 def parse(inp, filename=None):
907  """
908  Parse input or filename into a DOM tree.
909  If inp is None, open filename and load from there.
910  Otherwise, parse inp, either as string or file object.
911  If inp is already a DOM tree, this function is a noop.
912  :return:xml.dom.minidom.Document
913  :raise: xml.parsers.expat.ExpatError
914  """
915  f = None
916  if inp is None:
917  try:
918  inp = f = open(filename)
919  except IOError as e:
920  # do not report currently processed file as "in file ..."
921  filestack.pop()
922  raise XacroException(e.strerror + ": " + e.filename)
923 
924  try:
925  if isinstance(inp, _basestr):
926  return xml.dom.minidom.parseString(inp)
927  elif hasattr(inp, 'read'):
928  return xml.dom.minidom.parse(inp)
929  return inp
930 
931  finally:
932  if f:
933  f.close()
934 
935 
936 def process_doc(doc,
937  in_order=False, just_deps=False, just_includes=False,
938  mappings=None, xacro_ns=True, **kwargs):
939  global verbosity, do_check_order
940  verbosity = kwargs.get('verbosity', verbosity)
941  do_check_order = kwargs.get('do_check_order', do_check_order)
942 
943  # set substitution args
944  if mappings is not None:
945  substitution_args_context['arg'] = mappings
946 
947  global allow_non_prefixed_tags
948  allow_non_prefixed_tags = xacro_ns
949 
950  # if not yet defined: initialize filestack
951  if not filestack: restore_filestack([None])
952 
953  # inorder processing requires to process the whole document for deps too
954  # because filenames might be specified via properties or macro parameters
955  if (just_deps or just_includes) and not in_order:
956  process_includes(doc.documentElement)
957  return
958 
959  macros = {}
960  symbols = Table()
961  if not in_order:
962  # process includes, macros, and properties before evaluating stuff
963  process_includes(doc.documentElement)
964  grab_macros(doc, macros)
965  grab_properties(doc, symbols)
966 
967  eval_all(doc.documentElement, macros, symbols)
968 
969  # reset substitution args
970  substitution_args_context['arg'] = {}
971 
972  if do_check_order and symbols.redefined:
973  warning("Document is incompatible to --inorder processing.")
974  warning("The following properties were redefined after usage:")
975  for k, v in symbols.redefined.items():
976  message(k, "redefined in", v, color='yellow')
977 
978 
979 def open_output(output_filename):
980  if output_filename is None:
981  return sys.stdout
982  else:
983  dir_name = os.path.dirname(output_filename)
984  if dir_name:
985  try:
986  os.makedirs(dir_name)
987  except os.error:
988  # errors occur when dir_name exists or creation failed
989  # ignore error here; opening of file will fail if directory is still missing
990  pass
991 
992  try:
993  return open(output_filename, 'w')
994  except IOError as e:
995  raise XacroException("Failed to open output:", exc=e)
996 
997 
998 def print_location(filestack, err=None, file=sys.stderr):
999  macros = getattr(err, 'macros', []) if err else []
1000  msg = 'when instantiating macro:'
1001  for m in macros:
1002  name = m.body.getAttribute('name')
1003  location = '(%s)' % m.history[-1][-1]
1004  print(msg, name, location, file=file)
1005  msg = 'instantiated from:'
1006 
1007  msg = 'in file:' if macros else 'when processing file:'
1008  for f in reversed(filestack):
1009  if f is None: f = 'string'
1010  print(msg, f, file=file)
1011  msg = 'included from:'
1012 
1013 def process_file(input_file_name, **kwargs):
1014  """main processing pipeline"""
1015  # initialize file stack for error-reporting
1016  restore_filestack([input_file_name])
1017  # parse the document into a xml.dom tree
1018  doc = parse(None, input_file_name)
1019  # perform macro replacement
1020  process_doc(doc, **kwargs)
1021 
1022  # add xacro auto-generated banner
1023  banner = [xml.dom.minidom.Comment(c) for c in
1024  [" %s " % ('=' * 83),
1025  " | This document was autogenerated by xacro from %-30s | " % input_file_name,
1026  " | EDITING THIS FILE BY HAND IS NOT RECOMMENDED %-30s | " % "",
1027  " %s " % ('=' * 83)]]
1028  first = doc.firstChild
1029  for comment in banner:
1030  doc.insertBefore(comment, first)
1031 
1032  return doc
1033 
1034 def main():
1035  opts, input_file_name = process_args(sys.argv[1:])
1036  if opts.in_order == False and not opts.just_includes:
1037  warning("xacro: Traditional processing is deprecated. Switch to --inorder processing!")
1038  message("To check for compatibility of your document, use option --check-order.", color='yellow')
1039  message("For more infos, see http://wiki.ros.org/xacro#Processing_Order", color='yellow')
1040 
1041  try:
1042  # open and process file
1043  doc = process_file(input_file_name, **vars(opts))
1044  # open the output file
1045  out = open_output(opts.output)
1046 
1047  # error handling
1048  except xml.parsers.expat.ExpatError as e:
1049  error("XML parsing error: %s" % unicode(e), alt_text=None)
1050  if verbosity > 0:
1051  print_location(filestack, e)
1052  print(file=sys.stderr) # add empty separator line before error
1053  print("Check that:", file=sys.stderr)
1054  print(" - Your XML is well-formed", file=sys.stderr)
1055  print(" - You have the xacro xmlns declaration:",
1056  "xmlns:xacro=\"http://www.ros.org/wiki/xacro\"", file=sys.stderr)
1057  sys.exit(2) # indicate failure, but don't print stack trace on XML errors
1058 
1059  except Exception as e:
1060  msg = unicode(e)
1061  if not msg: msg = repr(e)
1062  error(msg)
1063  if verbosity > 0:
1064  print_location(filestack, e)
1065  if verbosity > 1:
1066  print(file=sys.stderr) # add empty separator line before error
1067  raise # create stack trace
1068  else:
1069  sys.exit(2) # gracefully exit with error condition
1070 
1071  # special output mode
1072  if opts.just_deps:
1073  out.write(" ".join(set(all_includes)))
1074  print()
1075  return
1076 
1077  # write output
1078  out.write(doc.toprettyxml(indent=' ', **encoding))
1079  print()
1080  # only close output file, but not stdout
1081  if opts.output:
1082  out.close()
def push_file(filename)
def __init__(self)
def __init__(self, args, kwargs)
def _setitem(self, key, value, unevaluated)
def check_deprecated_tag(tag_name)
def process_includes(elt, macros=None, symbols=None)
def grab_properties(elt, table)
def __init__(self, msg=None, suffix=None, exc=None, macro=None)
def __setitem__(self, key, value)
def load_yaml(filename)
def next_sibling_element(node)
Definition: xmlutils.py:43
def _eval_literal(value)
def abs_filename_spec(filename_spec)
def process_include(elt, macros, symbols, func)
def restore_filestack(oldstack)
def grab_property(elt, table)
def __init__(self, args, kwargs)
def first_child_element(elt)
Definition: xmlutils.py:36
def is_valid_name(name)
def import_xml_namespaces(parent, attributes)
def grab_macro(elt, macros)
def error(args, kwargs)
Definition: color.py:34
def reqd_attrs(tag, attrs)
Definition: xmlutils.py:96
def message(msg, args, kwargs)
Definition: color.py:21
def __getattr__(self, item)
def replace_node(node, by, content_only=False)
Definition: xmlutils.py:50
def is_include(elt)
def __contains__(self, key)
def warning(args, kwargs)
Definition: color.py:28
def process_args(argv, require_input=True)
Definition: cli.py:61
def __init__(self, parent=None)
def grab_macros(elt, macros)
def check_attrs(tag, required, optional)
Definition: xmlutils.py:110
def parse_macro_arg(s)
def __getitem__(self, key)
def __init__(self, parent=None)
def deprecated_tag(_issued=[False])
def lex(self, str)
def get_include_files(filename_spec, symbols)
def _resolve_(self, key)
def eval_extension(s)


xacro
Author(s): Stuart Glaser, William Woodall, Robert Haschke
autogenerated on Sat Jun 8 2019 18:04:14