Repository: creare-com/podpac

Issue:
Pipelines should create unique instances for nodes that are re-used
**Description**
When a node is re-used in a Pipeline, it may be evaluated more than once with different coordinates. The pipeline should still evaluate correctly, but debugging becomes unreliable. All of the nodes in a Pipeline should be unique instances.

**Describe the solution you'd like**
Nodes should be copied if necessary during Pipeline instantiation.

**Additional context**
We may additionally want to ensure that Algorithms and Compositors also don't re-use nodes, either throwing an exception (or warning) or just copying when necessary.

Discussion:

Patch:
diff --git a/podpac/core/pipeline/pipeline.py b/podpac/core/pipeline/pipeline.py
index 16249c00..59750353 100644
--- a/podpac/core/pipeline/pipeline.py
+++ b/podpac/core/pipeline/pipeline.py
@@ -9,10 +9,12 @@ import warnings
 import importlib
 from collections import OrderedDict
 import json
+from copy import deepcopy
 
 import traitlets as tl
 import numpy as np
 
+from podpac.core.settings import settings
 from podpac.core.utils import OrderedDictTrait, JSONEncoder
 from podpac.core.node import Node, NodeException
 from podpac.core.data.datasource import DataSource
@@ -65,9 +67,9 @@ class Pipeline(Node):
     @tl.validate('json')
     def _json_validate(self, proposal):
         s = proposal['value']
-        definition = json.loads(s)
+        definition = json.loads(s, object_pairs_hook=OrderedDict)
         parse_pipeline_definition(definition)
-        return json.dumps(json.loads(s), cls=JSONEncoder) # standardize
+        return json.dumps(json.loads(s, object_pairs_hook=OrderedDict), cls=JSONEncoder) # standardize
 
     @tl.validate('definition')
     def _validate_definition(self, proposal):
@@ -81,7 +83,6 @@ class Pipeline(Node):
 
     @tl.default('definition')
     def _definition_from_json(self):
-        print("definition from json")
         return json.loads(self.json, object_pairs_hook=OrderedDict)
 
     @tl.default('output')
@@ -285,4 +286,7 @@ def _get_subattr(nodes, name, ref):
     except (KeyError, AttributeError):
         raise PipelineError("'%s' references nonexistent node/attribute '%s'" % (name, ref))
     
+    if settings['DEBUG']:
+        attr = deepcopy(attr)
+
     return attr
\ No newline at end of file


Test patch:
diff --git a/podpac/core/pipeline/test/test.json b/podpac/core/pipeline/test/test.json
index c7f3c7e8..f3d5e8f8 100644
--- a/podpac/core/pipeline/test/test.json
+++ b/podpac/core/pipeline/test/test.json
@@ -1,7 +1,7 @@
 {
     "nodes": {
         "a": {
-            "node": "core.algorithm.algorithm.Arange"
+            "node": "algorithm.Arange"
         }
     },
     "output": {
diff --git a/podpac/core/pipeline/test/test_pipeline.py b/podpac/core/pipeline/test/test_pipeline.py
index 5b22bd6e..64e01b6d 100644
--- a/podpac/core/pipeline/test/test_pipeline.py
+++ b/podpac/core/pipeline/test/test_pipeline.py
@@ -32,7 +32,7 @@ class TestPipeline(object):
         {
             "nodes": {
                 "a": {
-                    "node": "core.algorithm.algorithm.Arange"
+                    "node": "algorithm.Arange"
                 }
             }
         }
@@ -48,7 +48,7 @@ class TestPipeline(object):
         {
             "nodes": {
                 "a": {
-                    "node": "core.algorithm.algorithm.Arange"
+                    "node": "algorithm.Arange"
                 }
             }
         }
@@ -69,7 +69,7 @@ class TestPipeline(object):
         {
             "nodes": {
                 "a": {
-                    "node": "core.algorithm.algorithm.Arange"
+                    "node": "algorithm.Arange"
                 }
             }
         }
@@ -90,7 +90,7 @@ class TestPipeline(object):
         {
             "nodes": {
                 "a": {
-                    "node": "core.algorithm.algorithm.Arange"
+                    "node": "algorithm.Arange"
                 }
             },
             "output": {
@@ -115,7 +115,7 @@ class TestPipeline(object):
         {
             "nodes": {
                 "a": {
-                    "node": "core.algorithm.algorithm.Arange"
+                    "node": "algorithm.Arange"
                 }
             },
             "output": {
@@ -131,4 +131,33 @@ class TestPipeline(object):
         pipeline.eval(coords)
         if pipeline.output.path is not None and os.path.isfile(pipeline.output.path):
             os.remove(pipeline.output.path)
-        assert pipeline.output.path is None
\ No newline at end of file
+        assert pipeline.output.path is None
+
+    def test_debuggable(self):
+        s = '''
+        {
+            "nodes": {
+                "a": {
+                    "node": "algorithm.Arange"
+                },
+                "mean": {
+                    "node": "algorithm.SpatialConvolution",
+                    "inputs": {"source": "a"},
+                    "attrs": {"kernel_type": "mean,3"}
+                },
+                "c": {
+                    "node": "algorithm.Arithmetic",
+                    "inputs": {"A": "a", "B": "mean"},
+                    "attrs": {"eqn": "a-b"}
+                }
+            }
+        }
+        '''
+
+        pipeline = Pipeline(json=s)
+        assert pipeline.node.A is pipeline.node.B.source
+
+        podpac.core.settings.settings['DEBUG'] = True
+        pipeline = Pipeline(json=s)
+        assert pipeline.node.A is not pipeline.node.B.source
+        podpac.core.settings.settings['DEBUG'] = False
\ No newline at end of file