Repository: altair-viz/altair

Issue:
`DatumType` and `Selection` instances are not deep copyable.
Thanks for your work on Altair, which makes generating VegaLite plots in Python super easy.

One issue I've come across is that you cannot deep-copy some of the object types because the `__getattr__` implementations of some objects are too broad (i.e. https://github.com/altair-viz/altair/blob/master/altair/expr/core.py#L14, https://github.com/altair-viz/altair/blob/d862ad494ecc0a5cff7e8b114df14e0796efa108/altair/vegalite/v3/api.py#L196, https://github.com/altair-viz/altair/blob/d862ad494ecc0a5cff7e8b114df14e0796efa108/altair/vegalite/v4/api.py#L196). These methods return a result for *any* request, including Python magic attributes of form `__<name>__`, which causes issues for modules like `copy.deepcopy` which looks for a `__deepcopy__` method on the object being copied to see if the copying behaviour has been overridden.

Would you be willing to accept patches to fix this? Or is this done intentionally?

Discussion:

Patch:
diff --git a/altair/expr/core.py b/altair/expr/core.py
index c1fdf49b..9e530b69 100644
--- a/altair/expr/core.py
+++ b/altair/expr/core.py
@@ -8,6 +8,8 @@ class DatumType(object):
         return "datum"
 
     def __getattr__(self, attr):
+        if attr.startswith("__") and attr.endswith("__"):
+            raise AttributeError(attr)
         return GetAttrExpression("datum", attr)
 
     def __getitem__(self, attr):
diff --git a/altair/vegalite/v3/api.py b/altair/vegalite/v3/api.py
index 23f79794..6473d41e 100644
--- a/altair/vegalite/v3/api.py
+++ b/altair/vegalite/v3/api.py
@@ -194,6 +194,8 @@ class Selection(object):
         return Selection(core.SelectionOr(**{"or": [self.name, other]}), self.selection)
 
     def __getattr__(self, field_name):
+        if field_name.startswith("__") and field_name.endswith("__"):
+            raise AttributeError(field_name)
         return expr.core.GetAttrExpression(self.name, field_name)
 
     def __getitem__(self, field_name):
diff --git a/altair/vegalite/v4/api.py b/altair/vegalite/v4/api.py
index 44f6f8e4..73c19361 100644
--- a/altair/vegalite/v4/api.py
+++ b/altair/vegalite/v4/api.py
@@ -194,6 +194,8 @@ class Selection(object):
         return Selection(core.SelectionOr(**{"or": [self.name, other]}), self.selection)
 
     def __getattr__(self, field_name):
+        if field_name.startswith("__") and field_name.endswith("__"):
+            raise AttributeError(field_name)
         return expr.core.GetAttrExpression(self.name, field_name)
 
     def __getitem__(self, field_name):


Test patch:
diff --git a/altair/expr/tests/test_expr.py b/altair/expr/tests/test_expr.py
index 17a2b98f..19265406 100644
--- a/altair/expr/tests/test_expr.py
+++ b/altair/expr/tests/test_expr.py
@@ -1,5 +1,7 @@
 import operator
 
+import pytest
+
 from ... import expr
 from .. import datum
 
@@ -95,6 +97,9 @@ def test_datum_getattr():
     x = datum["foo"]
     assert repr(x) == "datum['foo']"
 
+    with pytest.raises(AttributeError):
+        datum.__magic__
+
 
 def test_expression_getitem():
     x = datum.foo[0]
diff --git a/altair/vegalite/v3/tests/test_api.py b/altair/vegalite/v3/tests/test_api.py
index e8f072b8..489d8cf6 100644
--- a/altair/vegalite/v3/tests/test_api.py
+++ b/altair/vegalite/v3/tests/test_api.py
@@ -249,6 +249,9 @@ def test_selection_expression():
     assert isinstance(selection["value"], alt.expr.Expression)
     assert selection["value"].to_dict() == "{0}['value']".format(selection.name)
 
+    with pytest.raises(AttributeError):
+        selection.__magic__
+
 
 @pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf"])
 def test_save(format, basic_chart):
diff --git a/altair/vegalite/v4/tests/test_api.py b/altair/vegalite/v4/tests/test_api.py
index befb8d5e..830da2f9 100644
--- a/altair/vegalite/v4/tests/test_api.py
+++ b/altair/vegalite/v4/tests/test_api.py
@@ -249,6 +249,9 @@ def test_selection_expression():
     assert isinstance(selection["value"], alt.expr.Expression)
     assert selection["value"].to_dict() == "{0}['value']".format(selection.name)
 
+    with pytest.raises(AttributeError):
+        selection.__magic__
+
 
 @pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf"])
 def test_save(format, basic_chart):