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