Repository: holoviz/panel

Issue:
Call pipeline.init() automatically
The pipeline.init() method is confusing; it's hard to tell when it needs to be call, what it does, or why it can't be called automatically.  Basically, it introduces state that makes the user have to reason about it, without extensive docs that explain how to do that reasoning.

@philippjfr suggests that it might indeed be possible to avoid having to call it explicitly.  "The issue was that it wasn't obvious how to set up the graph when you declare a simple linear pipeline without calling define_graph, but if we simply build up the linear pipeline as stages are added and then override if define_graph is called there is no problem."


Discussion:

Resolving patch:
diff --git a/panel/pipeline.py b/panel/pipeline.py
--- a/panel/pipeline.py
+++ b/panel/pipeline.py
@@ -188,7 +188,7 @@ def __init__(self, stages=[], graph={}, **params):
             HSpacer()
         )
         self.stage = Row()
-        self._layout = Column(self.header, self.stage, sizing_mode='stretch_width')
+        self.layout = Column(self.header, self.stage, sizing_mode='stretch_width')
 
         # Initialize stages and the graph
         for stage in stages:
@@ -218,6 +218,9 @@ def __repr__(self):
             repr_str += '\n    [%d] %s: %s(%s)' % (i, name, cls_name, params)
         return repr_str
 
+    def __str__(self):
+        return self.__repr__()
+
     def __getitem__(self, index):
         return self._stages[index][0]
 
@@ -251,6 +254,7 @@ def _init_stage(self):
                 previous.append(src)
         prev_states = [self._states[prev] for prev in previous if prev in self._states]
 
+        outputs = []
         kwargs, results = {}, {}
         for state in prev_states:
             for name, (_, method, index) in state.param.outputs().items():
@@ -262,6 +266,7 @@ def _init_stage(self):
                 if index is not None:
                     result = result[index]
                 kwargs[name] = result
+                outputs.append(name)
             if stage_kwargs.get('inherit_params', self.inherit_params):
                 params = [k for k, v in state.param.objects('existing').items()
                               if v.precedence is None or v.precedence >= 0]
@@ -274,6 +279,10 @@ def _init_stage(self):
         else:
             self._state = stage(**kwargs)
 
+        # Hide widgets for parameters that are supplied by the previous stage
+        for output in outputs:
+            self._state.param[output].precedence = -1
+
         ready_param = stage_kwargs.get('ready_parameter', self.ready_parameter)
         if ready_param and ready_param in stage.param:
             self._state.param.watch(self._unblock, ready_param, onlychanged=False)
@@ -448,16 +457,19 @@ def _make_progress(self):
             tools=[], default_tools=['hover'], selection_policy=None,
             node_hover_fill_color='gray', backend='bokeh')
         labels = hv.Labels(nodes, ['x', 'y'], 'Stage').opts(
-            yoffset=-.30, backend='bokeh')
+            yoffset=-.30, default_tools=[], backend='bokeh')
         plot = (graph * labels) if self._linear else graph
         plot.opts(
-            xaxis=None, yaxis=None, min_width=600, responsive=True,
+            xaxis=None, yaxis=None, min_width=400, responsive=True,
             show_frame=False, height=height, xlim=(-0.25, depth+0.25), ylim=(0, 1),
             default_tools=['hover'], toolbar=None,
             backend='bokeh'
         )
         return plot
 
+    def _repr_mimebundle_(self, include=None, exclude=None):
+        return self.layout._repr_mimebundle_(include, exclude)
+
     #----------------------------------------------------------------
     # Public API
     #----------------------------------------------------------------
@@ -479,7 +491,21 @@ def add_stage(self, name, stage, **kwargs):
         for k in kwargs:
             if k not in self.param:
                 raise ValueError("Keyword argument %s is not a valid parameter. " % k)
+        
+        if not self._linear and self._graph:
+            raise RuntimeError("Cannot add stage after graph has been defined.")
+
         self._stages[name] = (stage, kwargs)
+        if len(self._stages) == 1:
+            self._stage = name
+            self._route = [name]
+            self._graph = {}
+            self.stage[:] = [self._init_stage()]
+        else:
+            previous = [s for s in self._stages if s not in self._graph][0]
+            self._graph[previous] = (name,)
+        self._update_progress()
+        self._update_button()
 
     def define_graph(self, graph, force=False):
         """
@@ -536,18 +562,4 @@ def define_graph(self, graph, force=False):
             ]
         self.stage[:] = [self._init_stage()]
         self._update_progress()
-
-    def init(self):
-        """
-        Initialize the Pipeline before first display.
-        """
-        if not self._graph:
-            self.define_graph(self._graph)
-        else:
-            self._update_progress()
         self._update_button()
-
-    @property
-    def layout(self):
-        self.init()
-        return self._layout


Test patch:
diff --git a/panel/tests/test_pipeline.py b/panel/tests/test_pipeline.py
--- a/panel/tests/test_pipeline.py
+++ b/panel/tests/test_pipeline.py
@@ -217,8 +217,6 @@ def test_pipeline_define_graph():
 
     pipeline.define_graph({'Stage 1': ('Stage 2', 'Stage 2b')})
 
-    pipeline.init()
-
     assert pipeline._stage == 'Stage 1'
 
     assert isinstance(pipeline.buttons, Row)
@@ -256,8 +254,6 @@ def test_pipeline_define_next_parameter_respected():
 
     pipeline.define_graph({'Stage 1': ('Stage 2', 'Stage 2b')})
 
-    pipeline.init()
-
     assert pipeline.next_selector.value == 'Stage 2b'
 
     pipeline._state.next = 'Stage 2'
@@ -274,8 +270,6 @@ def test_pipeline_error_condition():
 
     pipeline.define_graph({'Stage 1': ('Stage 2', 'Stage 2b')})
 
-    pipeline.init()
-
     pipeline.next_selector.value = 'Stage 2b'
     pipeline._next()
 
@@ -301,8 +295,6 @@ def test_pipeline_previous_follows_initial_path():
         'Stage 2b': 'Stage 3'
     })
 
-    pipeline.init()
-
     assert pipeline._route == ['Stage 1']
 
     pipeline.next_selector.value = 'Stage 2b'
@@ -325,8 +317,6 @@ def test_pipeline_ready_respected():
     pipeline.add_stage('Stage 1', Stage1)
     pipeline.add_stage('Stage 2', Stage2)
 
-    pipeline.init()
-
     assert pipeline.next_button.disabled
 
     pipeline._state.ready = True
@@ -339,8 +329,6 @@ def test_pipeline_auto_advance_respected():
     pipeline.add_stage('Stage 1', Stage1)
     pipeline.add_stage('Stage 2', Stage2)
 
-    pipeline.init()
-
     assert pipeline.next_button.disabled
 
     pipeline._state.ready = True
@@ -356,8 +344,6 @@ def test_pipeline_network_diagram_states():
 
     pipeline.define_graph({'Stage 1': ('Stage 2', 'Stage 2b')})
 
-    pipeline.init()
-
     [s1, s2, s2b] = pipeline.network.object.nodes['State']
 
     assert s1 == 'active'