33 from __future__
import print_function, division
41 import xml.dom.minidom
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
52 encoding = {
'encoding':
'utf-8'}
59 substitution_args_context = {}
76 Prepend the dirname of the currently processed file
77 if filename_spec is not yet absolute
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)
87 """Wrapper class for yaml lists to allow recursive inheritance of wrapper property"""
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):
94 elif isinstance(item, list):
100 return YamlListWrapper.wrap(super(YamlListWrapper, self).
__getitem__(idx))
103 for item
in super(YamlListWrapper, self).
__iter__():
104 yield YamlListWrapper.wrap(item)
108 """Wrapper class providing dotted access to dict items"""
111 return YamlListWrapper.wrap(super(YamlDictWrapper, self).
__getitem__(item))
113 raise AttributeError(
"The yaml dictionary has no key '{}'".format(item))
115 __getitem__ = __getattr__
119 """utility function to construct radian values from yaml"""
120 value = loader.construct_scalar(node)
122 return float(
safe_eval(value, _global_symbols))
128 """utility function for converting degrees into radians from yaml"""
135 yaml.SafeLoader.add_constructor(
u'!radians', construct_angle_radians)
136 yaml.SafeLoader.add_constructor(
u'!degrees', construct_angle_degrees)
138 raise XacroException(
"yaml support not available; install python-yaml")
142 filestack.append(filename)
144 return YamlListWrapper.wrap(yaml.safe_load(f))
149 all_includes.append(filename)
153 results = re.split(
'[{}]'.format(sep), s)
155 return [item
for item
in results
if item]
167 def deprecate(f, msg):
168 def wrapper(*args, **kwargs):
170 return f(*args, **kwargs)
172 return wrapper
if msg
else f
174 def expose(*args, **kwargs):
176 source, ns, deprecate_msg = (kwargs.pop(key,
None)
for key
in [
'source',
'ns',
'deprecate_msg'])
179 if source
is not None:
180 addons.update([(key, source[key])
for key
in args])
183 addons.update(**kwargs)
190 result.update([(ns, target)])
191 target.update(addons)
193 if deprecate_msg
is not None:
194 result.update([(key, deprecate(f, deprecate_msg.format(name=key, ns=ns)))
for key, f
in addons.items()])
196 result.update(addons)
198 deprecate_msg =
'Using {name}() directly is deprecated. Use {ns}.{name}() instead.'
200 expose(
'list',
'dict',
'map',
'len',
'str',
'float',
'int',
'True',
'False',
'min',
'max',
'round',
203 expose(
'sorted',
'range', source=__builtins__, ns=
'python', deprecate_msg=deprecate_msg)
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')
210 expose([(k, v)
for k, v
in math.__dict__.items()
if not k.startswith(
'_')], ns=
'math', deprecate_msg=
'')
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')
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)
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')
238 code = compile(expr.strip(),
"<expression>",
"eval")
239 invalid_names = [n
for n
in code.co_names
if n.startswith(
"__")]
241 raise XacroException(
"Use of invalid name(s): ",
', '.join(invalid_names))
242 globals.update(__builtins__= {})
243 return eval(code, globals, locals)
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
253 def __init__(self, msg=None, suffix=None, exc=None, macro=None):
254 super(XacroException, self).
__init__(msg)
257 self.
macros = []
if macro
is None else [macro]
261 return ' '.join([s
for s
in [
unicode(e)
for e
in items]
if s
not in [
'',
'None']])
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
277 allowed = required + optional
278 extra = [a
for a
in tag.attributes.keys()
if a
not in allowed
and not a.startswith(
"xmlns:")]
280 warning(
"%s: unknown attribute(s): %s" % (tag.nodeName,
', '.join(extra)))
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:
303 except ArgException
as e:
305 except ResourceNotFound
as e:
318 except AttributeError:
326 if isinstance(value, _basestr):
328 if len(value) >= 2
and value[0] ==
"'" and value[-1] ==
"'":
336 for f
in [int, float,
lambda x: get_boolean_value(x,
None)]:
348 'Consider disabling lazy evaluation via lazy_eval="false"'
349 .format(
" -> ".join(self.
recursive + [key])))
351 dict.__setitem__(self, key, self.
_eval_literal(eval_text(dict.__getitem__(self, key), self)))
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)
363 if dict.__contains__(self, key):
370 warning(
"redefining global symbol: %s" % key)
374 dict.__setitem__(self, key, value)
375 if unevaluated
and isinstance(value, _basestr):
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)
386 self.
_setitem(key, value, unevaluated=
True)
391 while p
is not self.
root:
392 dict.pop(p, key,
None)
395 warning(
'Cannot remove global symbol: ' + key)
399 dict.__contains__(self, key)
or (key
in self.
parent)
402 s = dict.__str__(self)
403 if self.
parent is not None:
410 while p.parent
is not p.root:
417 super(NameSpace, self).
__init__(parent)
424 raise NameError(
"name '{}' is not defined".format(item))
432 self.__dict__.update(other.__dict__)
435 for k, v
in kwargs.items():
436 self.__setattr__(k, len(self.
res))
437 self.
res.append(re.compile(v))
454 for i
in range(len(self.
res)):
455 m = self.
res[i].match(self.
str)
457 self.
top = (i, m.group(0))
458 self.
str = self.
str[m.end():]
464 include_no_matches_msg =
"""Include tag's filename spec \"{}\" matched no files."""
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')
476 if re.search(
'[*[?]+', filename_spec):
478 filenames = sorted(glob.glob(filename_spec))
479 if len(filenames) == 0:
480 warning(include_no_matches_msg.format(filename_spec))
483 filenames = [filename_spec]
485 for filename
in filenames:
487 all_includes.append(filename)
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]))
502 parent.setAttribute(name, value)
507 filename_spec, namespace_spec, optional =
check_attrs(elt, [
'filename'], [
'ns',
'optional'])
510 namespace_spec = eval_text(namespace_spec, symbols)
511 macros[namespace_spec] = ns_macros =
NameSpace()
512 symbols[namespace_spec] = ns_symbols =
NameSpace(parent=symbols)
514 raise XacroException(
'namespaces are supported with in-order option only')
519 optional = get_boolean_value(optional,
None)
522 warning(
"Child elements of a <xacro:include> tag are ignored")
529 filestack.append(filename)
530 include = parse(
None, filename).documentElement
533 func(include, ns_macros, ns_symbols)
534 included.append(include)
539 except XacroException
as e:
540 if e.exc
and isinstance(e.exc, IOError)
and optional
is True:
545 remove_previous_comments(elt)
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.
557 root = ast.parse(name)
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:
569 default_value =
'''\$\{.*?\}|\$\(.*?\)|(?:'.*?'|\".*?\"|[^\s'\"]+)+|'''
570 re_macro_arg = re.compile(
r'^\s*([^\s:=]+?)\s*:?=\s*(\^\|?)?(' + default_value +
')(?:\s+|$)(.*)')
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
584 m = re_macro_arg.match(s)
587 param, forward, default, rest = m.groups()
590 return param, (param
if forward
else None, default), rest
593 result = s.lstrip().split(
None, 1)
594 return result[0],
None, result[1]
if len(result) > 1
else ''
598 assert(elt.tagName ==
'xacro:macro')
599 remove_previous_comments(elt)
601 name, params =
check_attrs(elt, [
'name'], [
'params'])
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)
611 macro = macros.get(name,
Macro())
613 macro.history.append(deepcopy(filestack))
618 macro.defaultmap = {}
621 macro.params.append(param)
622 if value
is not None:
623 macro.defaultmap[param] = value
630 assert(elt.tagName ==
'xacro:property')
631 remove_previous_comments(elt)
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)
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)
644 if remove
and name
in table:
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:
665 lazy_eval = get_boolean_value(eval_text(lazy_eval
or 'true', table), lazy_eval)
667 if scope
and scope ==
'global':
668 target_table = table.top()
670 elif scope
and scope ==
'parent':
671 if table.parent
is not None:
672 target_table = table.parent
674 if not isinstance(table, NameSpace):
676 while isinstance(target_table, NameSpace):
677 target_table = target_table.parent
679 warning(
"%s: no parent scope at global scope " % name)
684 if not lazy_eval
and isinstance(value, _basestr):
685 value = eval_text(value, table)
687 target_table._setitem(name, value, unevaluated=lazy_eval)
690 LEXER =
QuickLexer(DOLLAR_DOLLAR_BRACE=
r"^\$\$+(\{|\()",
691 EXPR=
r"^\$\{[^\}]*\}",
692 EXTENSION=
r"^\$\([^\)]*\)",
693 TEXT=
r"[^$]+|\$[^{($]+|\$$")
697 def eval_text(text, symbols):
700 return safe_eval(eval_text(s, symbols), symbols)
701 except Exception
as e:
704 suffix=os.linesep +
"when evaluating expression '%s'" % s)
706 def handle_extension(s):
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]))
719 results.append(lex.next()[1])
720 elif id == lex.DOLLAR_DOLLAR_BRACE:
721 results.append(lex.next()[1][1:])
723 if len(results) == 1:
727 return ''.join(map(unicode, results))
730 def eval_default_arg(forward_variable, default, symbols, macro):
731 if forward_variable
is None:
732 return eval_text(default, symbols)
734 return symbols[forward_variable]
736 if default
is not None:
737 return eval_text(default, symbols)
739 raise XacroException(
"Undefined property to forward: " + forward_variable, macro=macro)
742 def handle_dynamic_macro_call(node, macros, symbols):
745 raise XacroException(
"xacro:call is missing the 'macro' attribute")
746 name =
unicode(eval_text(name, symbols))
749 node.removeAttribute(
'macro')
750 node.tagName =
'xacro:' + name
752 handle_macro_call(node, macros, symbols)
756 def resolve_macro(fullname, macros, symbols):
757 def _resolve(namespaces, name, macros, symbols):
759 for ns
in namespaces:
761 symbols = symbols[ns]
762 return macros, symbols, macros[name]
766 return _resolve([], fullname, macros, symbols)
769 namespaces = fullname.split(
'.')
770 name = namespaces.pop(-1)
772 return _resolve(namespaces, name, macros, symbols)
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:'):
783 name = node.tagName[6:]
785 macros, symbols, m = resolve_macro(name, macros, symbols)
786 body = m.body.cloneNode(deep=
True)
794 scoped_symbols =
Table(symbols)
795 scoped_macros =
Table(macros)
797 for name, value
in node.attributes.items():
798 if name
not in params:
801 scoped_symbols._setitem(name, eval_text(value, symbols), unevaluated=
False)
802 node.setAttribute(name,
"")
805 eval_all(node, macros, symbols)
809 for param
in params[:]:
814 scoped_symbols[param] = block
817 if block
is not None:
818 raise XacroException(
"Unused block \"%s\"" % block.tagName, macro=m)
821 for param
in params[:]:
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)
833 raise XacroException(
"Undefined parameters [%s]" %
",".join(params), macro=m)
835 eval_all(body, scoped_macros, scoped_symbols)
838 remove_previous_comments(node)
848 def get_boolean_value(value, condition):
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.
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.
861 if isinstance(value, _basestr):
862 if value ==
'true' or value ==
'True':
864 elif value ==
'false' or value ==
'False':
867 return bool(int(value))
871 raise XacroException(
"Xacro conditional \"%s\" evaluated to \"%s\", "
872 "which is not a boolean expression." % (condition, value))
875 _empty_text_node = xml.dom.minidom.getDOMImplementation().createDocument(
None,
"dummy",
None).createTextNode(
'\n\n')
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
883 if previous.nodeType == xml.dom.Node.TEXT_NODE
and \
884 previous.data.isspace()
and previous.data.count(
'\n') <= 1:
885 previous = previous.previousSibling
887 if previous
and previous.nodeType == xml.dom.Node.COMMENT_NODE:
889 previous = previous.previousSibling
890 node.parentNode.removeChild(comment)
894 if next
and _empty_text_node != next:
895 node.parentNode.insertBefore(_empty_text_node, next)
899 def eval_all(node, macros, symbols):
900 """Recursively evaluate node, expanding macros, replacing properties, and evaluating expressions"""
902 for name, value
in node.attributes.items():
903 if name.startswith(
'xacro:'):
904 node.removeAttribute(name)
906 result =
unicode(eval_text(value, symbols))
907 node.setAttribute(name, result)
911 node.removeAttribute(
'xmlns:xacro')
912 except xml.dom.NotFoundErr:
915 node = node.firstChild
916 eval_comments =
False
918 next = node.nextSibling
919 if node.nodeType == xml.dom.Node.ELEMENT_NODE:
920 eval_comments =
False
921 if node.tagName ==
'xacro:insert_block':
924 if (
"**" + name)
in symbols:
926 block = symbols[
'**' + name]
928 elif (
"*" + name)
in symbols:
930 block = symbols[
'*' + name]
936 block = block.cloneNode(deep=
True)
938 eval_all(block, macros, symbols)
941 elif node.tagName ==
'xacro:include':
944 elif node.tagName ==
'xacro:property':
947 elif node.tagName ==
'xacro:macro':
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))
955 remove_previous_comments(node)
958 elif node.tagName ==
'xacro:element':
959 name = eval_text(*
reqd_attrs(node, [
'xacro:name']), symbols=symbols)
963 node.removeAttribute(
'xacro:name')
964 node.nodeName = node.tagName = name
967 elif node.tagName ==
'xacro:attribute':
968 name, value = [eval_text(a, symbols)
for a
in reqd_attrs(node, [
'name',
'value'])]
972 node.parentNode.setAttribute(name, value)
975 elif node.tagName
in [
'xacro:if',
'xacro:unless']:
976 remove_previous_comments(node)
978 keep = get_boolean_value(eval_text(cond, symbols), cond)
979 if node.tagName
in [
'unless',
'xacro:unless']:
983 eval_all(node, macros, symbols)
988 elif handle_macro_call(node, macros, symbols):
992 eval_all(node, macros, symbols)
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
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
1004 node.data =
unicode(eval_text(node.data, symbols))
1011 def parse(inp, filename=None):
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
1023 inp = f = open(filename)
1024 except IOError
as e:
1030 if isinstance(inp, _basestr):
1031 return xml.dom.minidom.parseString(inp)
1032 elif hasattr(inp,
'read'):
1033 return xml.dom.minidom.parse(inp)
1041 def process_doc(doc, mappings=None, **kwargs):
1043 verbosity = kwargs.get(
'verbosity', verbosity)
1046 substitution_args_context[
'arg'] = {}
if mappings
is None else mappings
1053 symbols =
Table(_global_symbols)
1056 targetNS = doc.documentElement.getAttribute(
'xacro:targetNamespace')
1058 doc.documentElement.removeAttribute(
'xacro:targetNamespace')
1059 doc.documentElement.setAttribute(
'xmlns', targetNS)
1061 eval_all(doc.documentElement, macros, symbols)
1064 substitution_args_context[
'arg'] = {}
1067 def open_output(output_filename):
1068 if output_filename
is None:
1071 dir_name = os.path.dirname(output_filename)
1074 os.makedirs(dir_name)
1081 return open(output_filename,
'w')
1082 except IOError
as e:
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:'
1094 msg =
'in file:' if macrostack
else 'when processing file:'
1095 for f
in reversed(filestack
or []):
1098 print(msg, f, file=sys.stderr)
1099 msg =
'included from:'
1102 def process_file(input_file_name, **kwargs):
1103 """main processing pipeline"""
1107 doc = parse(
None, input_file_name)
1109 process_doc(doc, **kwargs)
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)
1131 doc = process_file(input_file_name, **vars(opts))
1133 out = open_output(opts.output)
1136 except xml.parsers.expat.ExpatError
as e:
1137 error(
"XML parsing error: %s" %
unicode(e), alt_text=
None)
1140 print(file=sys.stderr)
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)
1147 except Exception
as e:
1155 print(file=sys.stderr)
1162 out.write(
" ".join(set(all_includes)))
1167 out.write(doc.toprettyxml(indent=
' ', **encoding))