33 from __future__
import print_function, division
43 import xml.dom.minidom
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
54 encoding = {
'encoding':
'utf-8'}
61 substitution_args_context = {}
78 Prepend the dirname of the currently processed file
79 if filename_spec is not yet absolute
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)
89 """Wrapper class for yaml lists to allow recursive inheritance of wrapper property"""
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):
96 elif isinstance(item, list):
102 return YamlListWrapper.wrap(super(YamlListWrapper, self).
__getitem__(idx))
105 for item
in super(YamlListWrapper, self).
__iter__():
106 yield YamlListWrapper.wrap(item)
110 """Wrapper class providing dotted access to dict items"""
113 return YamlListWrapper.wrap(super(YamlDictWrapper, self).
__getitem__(item))
115 raise AttributeError(
"The yaml dictionary has no key '{}'".format(item))
117 __getitem__ = __getattr__
121 """utility enumeration to construct a values with a unit from yaml"""
122 __ConstructUnitsValue = collections.namedtuple(
'__ConstructUnitsValue', [
'tag',
'conversion_constant'])
133 """utility function to construct a values with a unit from yaml"""
134 value = loader.construct_scalar(node)
136 return float(
safe_eval(value, _global_symbols))*self.value.conversion_constant
144 for unit
in ConstructUnits:
145 yaml.SafeLoader.add_constructor(unit.value.tag, unit.constructor)
147 raise XacroException(
"yaml support not available; install python-yaml")
151 filestack.append(filename)
153 return YamlListWrapper.wrap(yaml.safe_load(f))
158 all_includes.append(filename)
162 results = re.split(
'[{}]'.format(sep), s)
164 return [item
for item
in results
if item]
176 def deprecate(f, msg):
177 def wrapper(*args, **kwargs):
179 return f(*args, **kwargs)
181 return wrapper
if msg
else f
183 def expose(*args, **kwargs):
185 source, ns, deprecate_msg = (kwargs.pop(key,
None)
for key
in [
'source',
'ns',
'deprecate_msg'])
188 if source
is not None:
189 addons.update([(key, source[key])
for key
in args])
192 addons.update(**kwargs)
199 result.update([(ns, target)])
200 target.update(addons)
202 if deprecate_msg
is not None:
203 result.update([(key, deprecate(f, deprecate_msg.format(name=key, ns=ns)))
for key, f
in addons.items()])
205 result.update(addons)
207 deprecate_msg =
'Using {name}() directly is deprecated. Use {ns}.{name}() instead.'
209 expose(
'list',
'dict',
'map',
'len',
'str',
'float',
'int',
'True',
'False',
'min',
'max',
'round',
212 expose(
'sorted',
'range', source=__builtins__, ns=
'python', deprecate_msg=deprecate_msg)
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')
219 expose([(k, v)
for k, v
in math.__dict__.items()
if not k.startswith(
'_')], ns=
'math', deprecate_msg=
'')
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')
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)
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')
247 code = compile(expr.strip(),
"<expression>",
"eval")
248 invalid_names = [n
for n
in code.co_names
if n.startswith(
"__")]
250 raise XacroException(
"Use of invalid name(s): ",
', '.join(invalid_names))
251 globals.update(__builtins__= {})
252 return eval(code, globals, locals)
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
262 def __init__(self, msg=None, suffix=None, exc=None, macro=None):
263 super(XacroException, self).
__init__(msg)
266 self.
macros = []
if macro
is None else [macro]
270 return ' '.join([s
for s
in [
unicode(e)
for e
in items]
if s
not in [
'',
'None']])
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
286 allowed = required + optional
287 extra = [a
for a
in tag.attributes.keys()
if a
not in allowed
and not a.startswith(
"xmlns:")]
289 warning(
"%s: unknown attribute(s): %s" % (tag.nodeName,
', '.join(extra)))
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:
312 except ArgException
as e:
314 except ResourceNotFound
as e:
327 except AttributeError:
335 if isinstance(value, _basestr):
337 if len(value) >= 2
and value[0] ==
"'" and value[-1] ==
"'":
345 for f
in [int, float,
lambda x: get_boolean_value(x,
None)]:
357 'Consider disabling lazy evaluation via lazy_eval="false"'
358 .format(
" -> ".join(self.
recursive + [key])))
360 dict.__setitem__(self, key, self.
_eval_literal(eval_text(dict.__getitem__(self, key), self)))
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)
372 if dict.__contains__(self, key):
379 warning(
"redefining global symbol: %s" % key)
383 dict.__setitem__(self, key, value)
384 if unevaluated
and isinstance(value, _basestr):
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)
395 self.
_setitem(key, value, unevaluated=
True)
400 while p
is not self.
root:
401 dict.pop(p, key,
None)
404 warning(
'Cannot remove global symbol: ' + key)
408 dict.__contains__(self, key)
or (key
in self.
parent)
411 s = dict.__str__(self)
412 if self.
parent is not None:
419 while p.parent
is not p.root:
426 super(NameSpace, self).
__init__(parent)
433 raise NameError(
"name '{}' is not defined".format(item))
441 self.__dict__.update(other.__dict__)
444 for k, v
in kwargs.items():
445 self.__setattr__(k, len(self.
res))
446 self.
res.append(re.compile(v))
463 for i
in range(len(self.
res)):
464 m = self.
res[i].match(self.
str)
466 self.
top = (i, m.group(0))
467 self.
str = self.
str[m.end():]
473 include_no_matches_msg =
"""Include tag's filename spec \"{}\" matched no files."""
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')
485 if re.search(
'[*[?]+', filename_spec):
487 filenames = sorted(glob.glob(filename_spec))
488 if len(filenames) == 0:
489 warning(include_no_matches_msg.format(filename_spec))
492 filenames = [filename_spec]
494 for filename
in filenames:
496 all_includes.append(filename)
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]))
511 parent.setAttribute(name, value)
516 filename_spec, namespace_spec, optional =
check_attrs(elt, [
'filename'], [
'ns',
'optional'])
519 namespace_spec = eval_text(namespace_spec, symbols)
520 macros[namespace_spec] = ns_macros =
NameSpace()
521 symbols[namespace_spec] = ns_symbols =
NameSpace(parent=symbols)
523 raise XacroException(
'namespaces are supported with in-order option only')
528 optional = get_boolean_value(optional,
None)
531 warning(
"Child elements of a <xacro:include> tag are ignored")
538 filestack.append(filename)
539 include = parse(
None, filename).documentElement
542 func(include, ns_macros, ns_symbols)
543 included.append(include)
548 except XacroException
as e:
549 if e.exc
and isinstance(e.exc, IOError)
and optional
is True:
554 remove_previous_comments(elt)
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.
566 root = ast.parse(name)
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:
578 default_value =
r'''\$\{.*?\}|\$\(.*?\)|(?:'.*?'|\".*?\"|[^\s'\"]+)+|'''
579 re_macro_arg = re.compile(
r'^\s*([^\s:=]+?)\s*:?=\s*(\^\|?)?(' + default_value +
r')(?:\s+|$)(.*)')
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
593 m = re_macro_arg.match(s)
596 param, forward, default, rest = m.groups()
599 return param, (param
if forward
else None, default), rest
602 result = s.lstrip().split(
None, 1)
603 return result[0],
None, result[1]
if len(result) > 1
else ''
607 assert(elt.tagName ==
'xacro:macro')
608 remove_previous_comments(elt)
610 name, params =
check_attrs(elt, [
'name'], [
'params'])
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)
620 macro = macros.get(name,
Macro())
622 macro.history.append(deepcopy(filestack))
627 macro.defaultmap = {}
630 macro.params.append(param)
631 if value
is not None:
632 macro.defaultmap[param] = value
639 assert(elt.tagName ==
'xacro:property')
640 remove_previous_comments(elt)
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)
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)
653 if remove
and name
in table:
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:
674 lazy_eval = get_boolean_value(eval_text(lazy_eval
or 'true', table), lazy_eval)
676 if scope
and scope ==
'global':
677 target_table = table.top()
679 elif scope
and scope ==
'parent':
680 if table.parent
is not None:
681 target_table = table.parent
683 if not isinstance(table, NameSpace):
685 while isinstance(target_table, NameSpace):
686 target_table = target_table.parent
688 warning(
"%s: no parent scope at global scope " % name)
693 if not lazy_eval
and isinstance(value, _basestr):
694 value = eval_text(value, table)
696 target_table._setitem(name, value, unevaluated=lazy_eval)
699 LEXER =
QuickLexer(DOLLAR_DOLLAR_BRACE=
r"^\$\$+(\{|\()",
700 EXPR=
r"^\$\{[^\}]*\}",
701 EXTENSION=
r"^\$\([^\)]*\)",
702 TEXT=
r"[^$]+|\$[^{($]+|\$$")
706 def eval_text(text, symbols):
709 return safe_eval(eval_text(s, symbols), symbols)
710 except Exception
as e:
713 suffix=os.linesep +
"when evaluating expression '%s'" % s)
715 def handle_extension(s):
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]))
728 results.append(lex.next()[1])
729 elif id == lex.DOLLAR_DOLLAR_BRACE:
730 results.append(lex.next()[1][1:])
732 if len(results) == 1:
736 return ''.join(map(unicode, results))
739 def eval_default_arg(forward_variable, default, symbols, macro):
740 if forward_variable
is None:
741 return eval_text(default, symbols)
743 return symbols[forward_variable]
745 if default
is not None:
746 return eval_text(default, symbols)
748 raise XacroException(
"Undefined property to forward: " + forward_variable, macro=macro)
751 def handle_dynamic_macro_call(node, macros, symbols):
754 raise XacroException(
"xacro:call is missing the 'macro' attribute")
755 name =
unicode(eval_text(name, symbols))
758 node.removeAttribute(
'macro')
759 node.tagName =
'xacro:' + name
761 handle_macro_call(node, macros, symbols)
765 def resolve_macro(fullname, macros, symbols):
766 def _resolve(namespaces, name, macros, symbols):
768 for ns
in namespaces:
770 symbols = symbols[ns]
771 return macros, symbols, macros[name]
775 return _resolve([], fullname, macros, symbols)
778 namespaces = fullname.split(
'.')
779 name = namespaces.pop(-1)
781 return _resolve(namespaces, name, macros, symbols)
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:'):
792 name = node.tagName[6:]
794 macros, symbols, m = resolve_macro(name, macros, symbols)
795 body = m.body.cloneNode(deep=
True)
803 scoped_symbols =
Table(symbols)
804 scoped_macros =
Table(macros)
806 for name, value
in node.attributes.items():
807 if name
not in params:
810 scoped_symbols._setitem(name, eval_text(value, symbols), unevaluated=
False)
811 node.setAttribute(name,
"")
814 eval_all(node, macros, symbols)
818 for param
in params[:]:
823 scoped_symbols[param] = block
826 if block
is not None:
827 raise XacroException(
"Unused block \"%s\"" % block.tagName, macro=m)
830 for param
in params[:]:
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)
842 raise XacroException(
"Undefined parameters [%s]" %
",".join(params), macro=m)
844 eval_all(body, scoped_macros, scoped_symbols)
847 remove_previous_comments(node)
857 def get_boolean_value(value, condition):
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.
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.
870 if isinstance(value, _basestr):
871 if value ==
'true' or value ==
'True':
873 elif value ==
'false' or value ==
'False':
876 return bool(int(value))
880 raise XacroException(
"Xacro conditional \"%s\" evaluated to \"%s\", "
881 "which is not a boolean expression." % (condition, value))
884 _empty_text_node = xml.dom.minidom.getDOMImplementation().createDocument(
None,
"dummy",
None).createTextNode(
'\n\n')
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
892 if previous.nodeType == xml.dom.Node.TEXT_NODE
and \
893 previous.data.isspace()
and previous.data.count(
'\n') <= 1:
894 previous = previous.previousSibling
896 if previous
and previous.nodeType == xml.dom.Node.COMMENT_NODE:
898 previous = previous.previousSibling
899 node.parentNode.removeChild(comment)
903 if next
and _empty_text_node != next:
904 node.parentNode.insertBefore(_empty_text_node, next)
908 def eval_all(node, macros, symbols):
909 """Recursively evaluate node, expanding macros, replacing properties, and evaluating expressions"""
911 for name, value
in node.attributes.items():
912 if name.startswith(
'xacro:'):
913 node.removeAttribute(name)
915 result =
unicode(eval_text(value, symbols))
916 node.setAttribute(name, result)
920 node.removeAttribute(
'xmlns:xacro')
921 except xml.dom.NotFoundErr:
924 node = node.firstChild
925 eval_comments =
False
927 next = node.nextSibling
928 if node.nodeType == xml.dom.Node.ELEMENT_NODE:
929 eval_comments =
False
930 if node.tagName ==
'xacro:insert_block':
933 if (
"**" + name)
in symbols:
935 block = symbols[
'**' + name]
937 elif (
"*" + name)
in symbols:
939 block = symbols[
'*' + name]
945 block = block.cloneNode(deep=
True)
947 eval_all(block, macros, symbols)
950 elif node.tagName ==
'xacro:include':
953 elif node.tagName ==
'xacro:property':
956 elif node.tagName ==
'xacro:macro':
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))
964 remove_previous_comments(node)
967 elif node.tagName ==
'xacro:element':
968 name = eval_text(*
reqd_attrs(node, [
'xacro:name']), symbols=symbols)
972 node.removeAttribute(
'xacro:name')
973 node.nodeName = node.tagName = name
976 elif node.tagName ==
'xacro:attribute':
977 name, value = [eval_text(a, symbols)
for a
in reqd_attrs(node, [
'name',
'value'])]
981 node.parentNode.setAttribute(name, value)
984 elif node.tagName
in [
'xacro:if',
'xacro:unless']:
985 remove_previous_comments(node)
987 keep = get_boolean_value(eval_text(cond, symbols), cond)
988 if node.tagName
in [
'unless',
'xacro:unless']:
992 eval_all(node, macros, symbols)
997 elif handle_macro_call(node, macros, symbols):
1001 eval_all(node, macros, symbols)
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
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
1013 node.data =
unicode(eval_text(node.data, symbols))
1020 def parse(inp, filename=None):
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
1032 inp = f = open(filename)
1033 except IOError
as e:
1039 if isinstance(inp, _basestr):
1040 return xml.dom.minidom.parseString(inp)
1041 elif hasattr(inp,
'read'):
1042 return xml.dom.minidom.parse(inp)
1050 def process_doc(doc, mappings=None, **kwargs):
1052 verbosity = kwargs.get(
'verbosity', verbosity)
1055 substitution_args_context[
'arg'] = {}
if mappings
is None else mappings
1062 symbols =
Table(_global_symbols)
1065 targetNS = doc.documentElement.getAttribute(
'xacro:targetNamespace')
1067 doc.documentElement.removeAttribute(
'xacro:targetNamespace')
1068 doc.documentElement.setAttribute(
'xmlns', targetNS)
1070 eval_all(doc.documentElement, macros, symbols)
1073 substitution_args_context[
'arg'] = {}
1076 def open_output(output_filename):
1077 if output_filename
is None:
1080 dir_name = os.path.dirname(output_filename)
1083 os.makedirs(dir_name)
1090 return open(output_filename,
'w')
1091 except IOError
as e:
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:'
1103 msg =
'in file:' if macrostack
else 'when processing file:'
1104 for f
in reversed(filestack
or []):
1107 print(msg, f, file=sys.stderr)
1108 msg =
'included from:'
1111 def process_file(input_file_name, **kwargs):
1112 """main processing pipeline"""
1116 doc = parse(
None, input_file_name)
1118 process_doc(doc, **kwargs)
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)
1140 doc = process_file(input_file_name, **vars(opts))
1142 out = open_output(opts.output)
1145 except xml.parsers.expat.ExpatError
as e:
1146 error(
"XML parsing error: %s" %
unicode(e), alt_text=
None)
1149 print(file=sys.stderr)
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)
1156 except Exception
as e:
1164 print(file=sys.stderr)
1171 out.write(
" ".join(set(all_includes)))
1176 out.write(doc.toprettyxml(indent=
' ', **encoding))