Repository: iris-hep/qastle

Issue:
copies of LINQ nodes are broken
As https://github.com/iris-hep/qastle/actions/runs/1199240447 shows, `copy.copy` and `copy.deepcopy` fail on the custom LINQ `ast.AST` nodes in `linq_util.py`. This is because the constructors have been overridden with required arguments, but `ast.AST.__reduce_ex__()` (called by `copy`) doesn't know about this. All built-in `ast.AST` subclasses have only optional arguments in their constructors. This bug is showing up in the ServiceX code generator, since `func_adl_uproot` will try to `deepcopy` any `ast.AST` given to it, and `qastle.text_ast_to_python_ast()` is used, which inserts these LINQ nodes.

Comments:

Patch:
diff --git a/.flake8 b/.flake8
index 3bfa1d3..ed2b68a 100644
--- a/.flake8
+++ b/.flake8
@@ -1,3 +1,4 @@
 [flake8]
+ignore = C901, E241, W503
 max-line-length = 99
 per-file-ignores = __init__.py:F401,F403 tests/*:F403,F405
diff --git a/qastle/linq_util.py b/qastle/linq_util.py
index dee513d..b123349 100644
--- a/qastle/linq_util.py
+++ b/qastle/linq_util.py
@@ -4,82 +4,51 @@ import ast
 
 
 class Where(ast.AST):
-    def __init__(self, source, predicate):
-        self._fields = ['source', 'predicate']
-        self.source = source
-        self.predicate = predicate
+    _fields = ['source', 'predicate']
 
 
 class Select(ast.AST):
-    def __init__(self, source, selector):
-        self._fields = ['source', 'selector']
-        self.source = source
-        self.selector = selector
+    _fields = ['source', 'selector']
 
 
 class SelectMany(ast.AST):
-    def __init__(self, source, selector):
-        self._fields = ['source', 'selector']
-        self.source = source
-        self.selector = selector
+    _fields = ['source', 'selector']
 
 
 class First(ast.AST):
-    def __init__(self, source):
-        self._fields = ['source']
-        self.source = source
+    _fields = ['source']
 
 
 class Aggregate(ast.AST):
-    def __init__(self, source, seed, func):
-        self._fields = ['source', 'seed', 'func']
-        self.source = source
-        self.seed = seed
-        self.func = func
+    _fields = ['source', 'seed', 'func']
 
 
 class Count(ast.AST):
-    def __init__(self, source):
-        self._fields = ['source']
-        self.source = source
+    _fields = ['source']
 
 
 class Max(ast.AST):
-    def __init__(self, source):
-        self._fields = ['source']
-        self.source = source
+    _fields = ['source']
 
 
 class Min(ast.AST):
-    def __init__(self, source):
-        self._fields = ['source']
-        self.source = source
+    _fields = ['source']
 
 
 class Sum(ast.AST):
-    def __init__(self, source):
-        self._fields = ['source']
-        self.source = source
+    _fields = ['source']
 
 
 class Zip(ast.AST):
-    def __init__(self, source):
-        self._fields = ['source']
-        self.source = source
+    _fields = ['source']
 
 
 class OrderBy(ast.AST):
-    def __init__(self, source, key_selector):
-        self._fields = ['source', 'key_selector']
-        self.source = source
-        self.key_selector = key_selector
+    _fields = ['source', 'key_selector']
 
 
 class Choose(ast.AST):
-    def __init__(self, source, n):
-        self._fields = ['source', 'n']
-        self.source = source
-        self.n = n
+    _fields = ['source', 'n']
 
 
 linq_operator_names = ('Where',


Test patch:
diff --git a/tests/test_linq_util.py b/tests/test_linq_util.py
index e4a6606..c603f3d 100644
--- a/tests/test_linq_util.py
+++ b/tests/test_linq_util.py
@@ -3,6 +3,7 @@ from .testing_util import *
 from qastle import *
 
 import ast
+import copy
 
 import pytest
 
@@ -92,6 +93,21 @@ def test_select_bad():
         insert_linq_nodes(ast.parse('the_source.Select(None)'))
 
 
+def test_select_copy():
+    select = Select('src', 'slctr')
+    select_copy = copy.copy(select)
+    assert select_copy.source == select.source and select_copy.selector == select.selector
+
+
+def test_select_deepcopy():
+    select = Select('src', 'slctr')
+    select_copy = copy.deepcopy(select)
+    assert select_copy.source == select.source and select_copy.selector == select.selector
+    select.source = 'src2'
+    select.selector = 'slctr2'
+    assert select_copy.source == 'src' and select_copy.selector == 'slctr'
+
+
 def test_selectmany():
     initial_ast = ast.parse("the_source.SelectMany('lambda row: row')")
     final_ast = insert_linq_nodes(initial_ast)