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


xacro
Author(s): Stuart Glaser, William Woodall, Robert Haschke
autogenerated on Sat Jul 20 2024 02:50:07