diff --git a/.travis.yml b/.travis.yml index 5b73eb17..815c851e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ sudo: enabled language: python python: - "2.7" + - "3.5" + - "3.6" # command to install dependencies install: diff --git a/src/xacro/__init__.py b/src/xacro/__init__.py index 0c152230..633f7f50 100644 --- a/src/xacro/__init__.py +++ b/src/xacro/__init__.py @@ -203,17 +203,13 @@ def __init__(self, parent=None): @staticmethod def _eval_literal(value): if isinstance(value, _basestr): - try: - # try to evaluate as literal, e.g. number, boolean, etc. - # this is needed to handle numbers in property definitions as numbers, not strings - evaluated = ast.literal_eval(value.strip()) - # However, (simple) list, tuple, dict expressions will be evaluated as such too, - # which would break expected behaviour. Thus we only accept the evaluation otherwise. - if not isinstance(evaluated, (list, dict, tuple)): - return evaluated - except: - pass - + # try to evaluate as number literal or boolean + # this is needed to handle numbers in property definitions as numbers, not strings + for f in [int, float, lambda x: get_boolean_value(x, None)]: # order of types is important! + try: + return f(value) + except: + pass return value def _resolve_(self, key): @@ -577,10 +573,10 @@ def grab_properties(elt, table): elt = next -LEXER = QuickLexer(DOLLAR_DOLLAR_BRACE=r"\$\$+\{", - EXPR=r"\$\{[^\}]*\}", - EXTENSION=r"\$\([^\)]*\)", - TEXT=r"([^\$]|\$[^{(]|\$$)+") +LEXER = QuickLexer(DOLLAR_DOLLAR_BRACE=r"^\$\$+(\{|\()", # multiple $ in a row, followed by { or ( + EXPR=r"^\$\{[^\}]*\}", # stuff starting with ${ + EXTENSION=r"^\$\([^\)]*\)", # stuff starting with $( + TEXT=r"[^$]+|\$[^{($]+|\$$") # any text w/o $ or $ following any chars except {($ or single $ # evaluate text and return typed value @@ -600,13 +596,14 @@ def handle_extension(s): lex = QuickLexer(LEXER) lex.lex(text) while lex.peek(): - if lex.peek()[0] == lex.EXPR: + id = lex.peek()[0] + if id == lex.EXPR: results.append(handle_expr(lex.next()[1][2:-1])) - elif lex.peek()[0] == lex.EXTENSION: + elif id == lex.EXTENSION: results.append(handle_extension(lex.next()[1][2:-1])) - elif lex.peek()[0] == lex.TEXT: + elif id == lex.TEXT: results.append(lex.next()[1]) - elif lex.peek()[0] == lex.DOLLAR_DOLLAR_BRACE: + elif id == lex.DOLLAR_DOLLAR_BRACE: results.append(lex.next()[1][1:]) # return single element as is, i.e. typed if len(results) == 1: @@ -750,9 +747,9 @@ def get_boolean_value(value, condition): """ try: if isinstance(value, _basestr): - if value == 'true': return True - elif value == 'false': return False - else: return ast.literal_eval(value) + if value == 'true' or value == 'True': return True + elif value == 'false' or value == 'False': return False + else: return bool(int(value)) else: return bool(value) except: @@ -950,7 +947,7 @@ def process_doc(doc, if do_check_order and symbols.redefined: warning("Document is incompatible to --inorder processing.") warning("The following properties were redefined after usage:") - for k, v in symbols.redefined.iteritems(): + for k, v in symbols.redefined.items(): message(k, "redefined in", v, color='yellow') diff --git a/src/xacro/xmlutils.py b/src/xacro/xmlutils.py index fe310143..df891097 100644 --- a/src/xacro/xmlutils.py +++ b/src/xacro/xmlutils.py @@ -30,7 +30,7 @@ # Authors: Stuart Glaser, William Woodall, Robert Haschke # Maintainer: Morgan Quigley -import xml +import xml.dom.minidom from .color import warning def first_child_element(elt): diff --git a/test/test_xacro.py b/test/test_xacro.py index 450d456a..0fa8a726 100644 --- a/test/test_xacro.py +++ b/test/test_xacro.py @@ -12,44 +12,70 @@ import shutil import subprocess import re -from cStringIO import StringIO +import ast +try: + from cStringIO import StringIO # Python 2.x +except ImportError: + from io import StringIO # Python 3.x from contextlib import contextmanager # regex to match whitespace whitespace = re.compile(r'\s+') +def text_values_match(a, b): + # generic comparison + if whitespace.sub(' ', a).strip() == whitespace.sub(' ', b).strip(): + return True + + try: # special handling of dicts: ignore order + a_dict = ast.literal_eval(a) + b_dict = ast.literal_eval(b) + if (isinstance(a_dict, dict) and isinstance(b_dict, dict) and a_dict == b_dict): + return True + except: # Attribute values aren't dicts + pass + + # on failure, try to split a and b at whitespace and compare snippets + def match_splits(a_, b_): + if len(a_) != len(b_): return False + for a, b in zip(a_, b_): + if a == b: continue + try: # compare numeric values only up to some accuracy + if abs(float(a) - float(b)) > 1.0e-9: + return False + except ValueError: # values aren't numeric and not identical + return False + return True + + return match_splits(a.split(), b.split()) + + def all_attributes_match(a, b): if len(a.attributes) != len(b.attributes): print("Different number of attributes") return False - a_atts = [(a.attributes.item(i).name, a.attributes.item(i).value) for i in range(len(a.attributes))] - b_atts = [(b.attributes.item(i).name, b.attributes.item(i).value) for i in range(len(b.attributes))] + a_atts = a.attributes.items() + b_atts = b.attributes.items() a_atts.sort() b_atts.sort() - for i in range(len(a_atts)): - if a_atts[i][0] != b_atts[i][0]: - print("Different attribute names: %s and %s" % (a_atts[i][0], b_atts[i][0])) + for a, b in zip(a_atts, b_atts): + if a[0] != b[0]: + print("Different attribute names: %s and %s" % (a[0], b[0])) + return False + if not text_values_match(a[1], b[1]): + print("Different attribute values: %s and %s" % (a[1], b[1])) return False - try: - if abs(float(a_atts[i][1]) - float(b_atts[i][1])) > 1.0e-9: - print("Different attribute values: %s and %s" % (a_atts[i][1], b_atts[i][1])) - return False - except ValueError: # Attribute values aren't numeric - if a_atts[i][1] != b_atts[i][1]: - print("Different attribute values: %s and %s" % (a_atts[i][1], b_atts[i][1])) - return False - return True + def text_matches(a, b): - a_norm = whitespace.sub(' ', a) - b_norm = whitespace.sub(' ', b) - if a_norm.strip() == b_norm.strip(): return True + if text_values_match(a, b): return True print("Different text values: '%s' and '%s'" % (a, b)) return False + def nodes_match(a, b, ignore_nodes): if not a and not b: return True @@ -138,6 +164,16 @@ def test_normalize_whitespace_text(self): def test_normalize_whitespace_trim(self): self.assertTrue(text_matches(" foo bar ", "foo \t\n\r bar")) + def test_match_similar_numbers(self): + self.assertTrue(text_matches("0.123456789", "0.123456788")) + def test_mismatch_different_numbers(self): + self.assertFalse(text_matches("0.123456789", "0.1234567879")) + + def test_match_unordered_dicts(self): + self.assertTrue(text_matches("{'a': 1, 'b': 2, 'c': 3}", "{'c': 3, 'b': 2, 'a': 1}")) + def test_mismatch_different_dicts(self): + self.assertFalse(text_matches("{'a': 1, 'b': 2, 'c': 3}", "{'c': 3, 'b': 2, 'a': 0}")) + def test_empty_node_vs_whitespace(self): self.assertTrue(xml_matches('''''', ''' \t\n\r ''')) def test_whitespace_vs_empty_node(self): @@ -167,8 +203,8 @@ def test_is_valid_name(self): def test_resolve_macro(self): # define three nested macro dicts with the same macro names (keys) content = {'xacro:simple': 'simple'} - ns2 = dict({k: v+'2' for k,v in content.iteritems()}) - ns1 = dict({k: v+'1' for k,v in content.iteritems()}) + ns2 = dict({k: v+'2' for k,v in content.items()}) + ns1 = dict({k: v+'1' for k,v in content.items()}) ns1.update(ns2=ns2) macros = dict(content) macros.update(ns1=ns1) @@ -411,13 +447,13 @@ def test_substitution_args_arg(self): def test_escaping_dollar_braces(self): self.assert_matches( - self.quick_xacro(''''''), - '''''') + self.quick_xacro(''''''), + '''''') def test_just_a_dollar_sign(self): self.assert_matches( - self.quick_xacro(''''''), - '''''') + self.quick_xacro(''''''), + '''''') def test_multiple_insert_blocks(self): self.assert_matches( diff --git a/xacro.py b/xacro.py index f162aff3..e6d1e10f 100755 --- a/xacro.py +++ b/xacro.py @@ -50,7 +50,7 @@ this_dir_cwd = os.getcwd() os.chdir(cur_dir) # Remove this dir from path -sys.path = filter(lambda a: a not in [this_dir, this_dir_cwd], sys.path) +sys.path = [a for a in sys.path if a not in [this_dir, this_dir_cwd]] import xacro from xacro.color import warning