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


xacro
Author(s): Stuart Glaser, William Woodall, Robert Haschke
autogenerated on Fri Jan 26 2024 03:50:16