Repository: altair-viz/altair
Pull request #574 (2018-03-13 15:27:03)

BUG: fix isse with to_dict() in expressions
Previously there was a recursion error when calling the ``copy()`` method on ``expr`` objects, because they weren't being initialized properly.


Patch:
diff --git a/altair/expr/core.py b/altair/expr/core.py
index 7c3df7bc..ed9dc487 100644
--- a/altair/expr/core.py
+++ b/altair/expr/core.py
@@ -89,11 +89,11 @@ class Expression(SchemaBase):
 
     def __pow__(self, other):
         # "**" Javascript operator is not supported in all browsers
-        return FunctionExpression('pow', self, other)
+        return FunctionExpression('pow', (self, other))
 
     def __rpow__(self, other):
         # "**" Javascript operator is not supported in all browsers
-        return FunctionExpression('pow', other, self)
+        return FunctionExpression('pow', (other, self))
 
     def __neg__(self):
         return UnaryExpression('-', self)
@@ -122,7 +122,7 @@ class Expression(SchemaBase):
         return BinaryExpression("<=", self, other)
 
     def __abs__(self):
-        return FunctionExpression('abs', self)
+        return FunctionExpression('abs', (self,))
 
     # logical operators
 
@@ -144,8 +144,7 @@ class Expression(SchemaBase):
 
 class UnaryExpression(Expression):
     def __init__(self, op, val):
-        self.op = op
-        self.val = val
+        super(UnaryExpression, self).__init__(op=op, val=val)
 
     def __repr__(self):
         return "({op}{val})".format(op=self.op, val=_js_repr(self.val))
@@ -153,9 +152,7 @@ class UnaryExpression(Expression):
 
 class BinaryExpression(Expression):
     def __init__(self, op, lhs, rhs):
-        self.op = op
-        self.lhs = lhs
-        self.rhs = rhs
+        super(BinaryExpression, self).__init__(op=op, lhs=lhs, rhs=rhs)
 
     def __repr__(self):
         return "({lhs} {op} {rhs})".format(op=self.op,
@@ -164,9 +161,8 @@ class BinaryExpression(Expression):
 
 
 class FunctionExpression(Expression):
-    def __init__(self, name, *args):
-        self.name = name
-        self.args = args
+    def __init__(self, name, args):
+        super(FunctionExpression, self).__init__(name=name, args=args)
 
     def __repr__(self):
         args = ','.join(_js_repr(arg) for arg in self.args)
@@ -175,9 +171,8 @@ class FunctionExpression(Expression):
 
 class ConstExpression(Expression):
     def __init__(self, name, doc):
-        self.name = name
-        self.doc = doc
         self.__doc__ = """{0}: {1}""".format(name, doc)
+        super(ConstExpression, self).__init__(name=name, doc=doc)
 
     def __repr__(self):
         return str(self.name)
@@ -185,7 +180,7 @@ class ConstExpression(Expression):
 
 class ValueExpression(Expression):
     def __init__(self, name):
-        self.name = name
+        super(ValueExpression, self).__init__(name=name)
 
     def __repr__(self):
         return "datum.{0}".format(self.name)
diff --git a/altair/expr/funcs.py b/altair/expr/funcs.py
index 25f95aaa..68f901ac 100644
--- a/altair/expr/funcs.py
+++ b/altair/expr/funcs.py
@@ -85,7 +85,7 @@ class ExprFunc(object):
         self.__doc__ = """{0}(*args)\n    {1}""".format(name, doc)
 
     def __call__(self, *args):
-        return FunctionExpression(self.name, *args)
+        return FunctionExpression(self.name, args)
 
     def __repr__(self):
         return "<function expr.{0}(*args)>".format(self.name)
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index 6cafb804..5a0b282e 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -207,7 +207,7 @@ class SchemaBase(object):
         def _todict(val):
             if isinstance(val, SchemaBase):
                 return val.to_dict(validate=sub_validate, context=context)
-            elif isinstance(val, list):
+            elif isinstance(val, (list, tuple)):
                 return [_todict(v) for v in val]
             elif isinstance(val, dict):
                 return {k: _todict(v) for k, v in val.items()
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index 2f92aec9..ed6757ea 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -205,7 +205,7 @@ class SchemaBase(object):
         def _todict(val):
             if isinstance(val, SchemaBase):
                 return val.to_dict(validate=sub_validate, context=context)
-            elif isinstance(val, list):
+            elif isinstance(val, (list, tuple)):
                 return [_todict(v) for v in val]
             elif isinstance(val, dict):
                 return {k: _todict(v) for k, v in val.items()


Test patch:
diff --git a/altair/expr/tests/test_expr.py b/altair/expr/tests/test_expr.py
index ac5062b1..ec5d9e0a 100644
--- a/altair/expr/tests/test_expr.py
+++ b/altair/expr/tests/test_expr.py
@@ -76,3 +76,9 @@ def test_json_reprs():
 def test_to_dict():
     ex = (datum.xxx * 2 > datum.yyy)
     assert ex.to_dict() == repr(ex)
+
+
+def test_copy():
+    ex = datum.xxx * 2 > abs(datum.yyy)
+    ex_copy = ex.copy()
+    assert ex.to_dict() == ex_copy.to_dict()