Repository: holoviz/panel

Issue:
Rows and Columns should have the `extend` method
To update the contents of a Row or Columns, you can use list methods like append and pop. For instance:

```python
import panel as pn

col = pn.Column('123')
col.append('new')
col.pop(col[0])
col
```
<img width="320" alt="screen shot 2019-03-05 at 2 32 10 pm" src="https://user-images.githubusercontent.com/4806877/53831779-9b309a80-3f53-11e9-9b33-203bf9c2a646.png">

It'd be nice to also accept `col.extend`



Discussion:

Resolving patch:
diff --git a/panel/layout.py b/panel/layout.py
--- a/panel/layout.py
+++ b/panel/layout.py
@@ -12,7 +12,7 @@
 
 from .io import state
 from .util import param_name, param_reprs, push
-from .viewable import Reactive, Viewable
+from .viewable import Reactive
 
 
 class Panel(Reactive):
@@ -196,6 +196,15 @@ def append(self, pane):
         new_objects.append(panel(pane))
         self.objects = new_objects
 
+    def clear(self):
+        self.objects = []
+
+    def extend(self, panes):
+        from .pane import panel
+        new_objects = list(self)
+        new_objects.extend(list(map(panel, panes)))
+        self.objects = new_objects
+
     def insert(self, index, pane):
         from .pane import panel
         new_objects = list(self)
@@ -214,6 +223,10 @@ def remove(self, pane):
         new_objects.remove(pane)
         self.objects = new_objects
 
+    def reverse(self):
+        new_objects = list(self)
+        new_objects.reverse()
+        self.objects = new_objects
 
 
 class Row(Panel):
@@ -254,21 +267,27 @@ class Tabs(Panel):
     _linked_props = ['active']
 
     def __init__(self, *items, **params):
+        objects, self._names = self._to_objects_and_names(items)
+        super(Tabs, self).__init__(*objects, **params)
+        self.param.watch(self._update_names, 'objects')
+
+    def _to_object_and_name(self, item):
         from .pane import panel
+        if isinstance(item, tuple):
+            name, item = item
+        else:
+            name = getattr(item, 'name', None)
+        pane = panel(item, name=name)
+        name = param_name(pane.name) if name is None else name
+        return pane, name
+
+    def _to_objects_and_names(self, items):
         objects, names = [], []
-        for pane in items:
-            if isinstance(pane, tuple):
-                name, pane = pane
-            elif isinstance(pane, Viewable):
-                name = pane.name
-            else:
-                name = None
-            objects.append(panel(pane, name=name))
-            name = param_name(objects[-1].name) if name is None else name
+        for item in items:
+            pane, name = self._to_object_and_name(item)
+            objects.append(pane)
             names.append(name)
-        self._names = names
-        super(Tabs, self).__init__(*objects, **params)
-        self.param.watch(self._update_names, 'objects')
+        return objects, names
 
     def _update_names(self, event):
         if len(event.new) == len(self._names):
@@ -296,18 +315,17 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
                              'directly. Found %d names, expected %d.' %
                              (len(self._names), len(self)))
         for i, (name, pane) in enumerate(zip(self._names, self)):
-            pane = panel(pane)
+            pane = panel(pane, name=name)
             self.objects[i] = pane
             if pane in old_objects:
                 child = pane._models[root.ref['id']]
             else:
                 child = pane._get_model(doc, root, model, comm)
-            child = BkPanel(title=name, child=child)
+            child = BkPanel(title=name, name=pane.name, child=child)
             new_models.append(child)
         return new_models
 
     def __setitem__(self, index, panes):
-        from .pane import panel
         new_objects = list(self)
         if not isinstance(index, slice):
             if index > len(self.objects):
@@ -340,34 +358,32 @@ def __setitem__(self, index, panes):
                                  'on the %s to match the supplied slice.' %
                                  (expected, type(self).__name__))
         for i, pane in zip(range(start, end), panes):
-            name = None
-            if isinstance(pane, tuple):
-                name, pane = pane
-            new_objects[i] = panel(pane, name=name)
-            name = param_name(new_objects[i].name) if name is None else name
-            self._names[i] = name
+            new_objects[i], self._names[i] = self._to_object_and_name(pane)
         self.objects = new_objects
 
     def append(self, pane):
-        from .pane import panel
-        name = None
-        if isinstance(pane, tuple):
-            name, pane = pane
+        new_object, new_name = self._to_object_and_name(pane)
         new_objects = list(self)
-        new_objects.append(panel(pane, name=name))
-        name = param_name(new_objects[-1].name) if name is None else name
-        self._names.append(name)
+        new_objects.append(new_object)
+        self._names.append(new_name)
         self.objects = new_objects
 
+    def clear(self):
+        self._names = []
+        self.objects = []
+
+    def extend(self, panes):
+        new_objects, new_names = self._to_objects_and_names(panes)
+        objects = list(self)
+        objects.extend(new_objects)
+        self._names.extend(new_names)
+        self.objects = objects
+
     def insert(self, index, pane):
-        from .pane import panel
-        name = None
-        if isinstance(pane, tuple):
-            name, pane = pane
+        new_object, new_name = self._to_object_and_name(pane)
         new_objects = list(self.objects)
-        new_objects.insert(index, panel(pane))
-        name = param_name(new_objects[index].name) if name is None else name
-        self._names[index] = name
+        new_objects.insert(index, new_object)
+        self._names.insert(index, new_name)
         self.objects = new_objects
 
     def pop(self, index):
@@ -386,6 +402,12 @@ def remove(self, pane):
         self._names.pop(index)
         self.objects = new_objects
 
+    def reverse(self):
+        new_objects = list(self)
+        new_objects.reverse()
+        self._names.reverse()
+        self.objects = new_objects
+
 
 class Spacer(Reactive):
     """Empty object used to control formatting (using positive or negative space)"""
diff --git a/panel/links.py b/panel/links.py
--- a/panel/links.py
+++ b/panel/links.py
@@ -7,7 +7,8 @@
 import weakref
 import sys
 
-from .layout import Viewable, Panel
+from .viewable import Viewable
+from .layout import Panel
 from .pane.holoviews import HoloViews, generate_panel_bokeh_map, is_bokeh_element_plot
 from .util import unicode_repr
 
@@ -90,7 +91,7 @@ def unlink(self):
         links = self.registry.get(self.source)
         if self in links:
             links.pop(links.index(self))
-    
+
     @classmethod
     def _process_links(cls, root_view, root_model):
         if not isinstance(root_view, Panel) or not root_model:


Test patch:
diff --git a/panel/tests/test_layout.py b/panel/tests/test_layout.py
--- a/panel/tests/test_layout.py
+++ b/panel/tests/test_layout.py
@@ -8,6 +8,18 @@
 from panel.pane import Bokeh, Pane
 from panel.param import Param
 
+@pytest.fixture
+def tabs(document, comm):
+    """Set up a tabs instance"""
+    div1, div2 = Div(), Div()
+
+    return Tabs(('Tab1', div1), ('Tab2', div2))
+
+def assert_tab_is_similar(tab1, tab2):
+    """Helper function to check tab match"""
+    assert tab1.child is tab2.child
+    assert tab1.name == tab2.name
+    assert tab1.title == tab2.title
 
 def get_div(box):
     # Temporary utilities to unpack widget boxes
@@ -85,6 +97,18 @@ def test_layout_get_root(panel, model_type, document, comm):
     assert model.children == [div1, div2]
 
 
+@pytest.mark.parametrize('panel', [Column, Row])
+def test_layout_reverse(panel, document, comm):
+    div1 = Div()
+    div2 = Div()
+    layout = panel(div1, div2)
+
+    model = layout._get_root(document, comm=comm)
+
+    layout.reverse()
+    assert model.children == [div2, div1]
+
+
 @pytest.mark.parametrize('panel', [Column, Row])
 def test_layout_append(panel, document, comm):
     div1 = Div()
@@ -98,6 +122,20 @@ def test_layout_append(panel, document, comm):
     assert model.children == [div1, div2, div3]
 
 
+@pytest.mark.parametrize('panel', [Column, Row])
+def test_layout_extend(panel, document, comm):
+    div1 = Div()
+    div2 = Div()
+    layout = panel(div1, div2)
+
+    model = layout._get_root(document, comm=comm)
+
+    div3 = Div()
+    div4 = Div()
+    layout.extend([div4, div3])
+    assert model.children == [div1, div2, div4, div3]
+
+
 @pytest.mark.parametrize('panel', [Column, Row])
 def test_layout_insert(panel, document, comm):
     div1 = Div()
@@ -256,6 +294,36 @@ def test_layout_remove(panel, document, comm):
     assert p1._models == {}
 
 
+@pytest.mark.parametrize('panel', [Column, Row])
+def test_layout_clear(panel, document, comm):
+    div1 = Div()
+    div2 = Div()
+    layout = panel(div1, div2)
+    p1, p2 = layout.objects
+
+    model = layout._get_root(document, comm=comm)
+
+    assert model.ref['id'] in p1._callbacks
+    assert p1._models[model.ref['id']] is model.children[0]
+    layout.clear()
+    assert model.children == []
+    assert p1._callbacks == p2._callbacks == {}
+    assert p1._models == p2._models == {}
+
+
+def test_tabs_basic_constructor(document, comm):
+    tabs = Tabs('plain', 'text')
+
+    model = tabs._get_root(document, comm=comm)
+
+    assert isinstance(model, BkTabs)
+    assert len(model.tabs) == 2
+    assert all(isinstance(c, BkPanel) for c in model.tabs)
+    tab1, tab2 = model.tabs
+
+    assert 'plain' in tab1.child.text
+    assert 'text' in tab2.child.text
+
 def test_tabs_constructor(document, comm):
     div1 = Div()
     div2 = Div()
@@ -288,9 +356,30 @@ def test_tabs_implicit_constructor(document, comm):
     assert all(isinstance(c, BkPanel) for c in model.tabs)
     tab1, tab2 = model.tabs
 
-    assert tab1.title == 'Div1'
+    assert tab1.title == tab1.name == p1.name == 'Div1'
     assert tab1.child is div1
-    assert tab2.title == 'Div2'
+    assert tab2.title == tab2.name == p2.name == 'Div2'
+    assert tab2.child is div2
+
+
+def test_tabs_constructor_with_named_objects(document, comm):
+    div1, div2 = Div(), Div()
+    p1 = Pane(div1, name='Div1')
+    p2 = Pane(div2, name='Div2')
+    tabs = Tabs(('Tab1', p1), ('Tab2', p2))
+
+    model = tabs._get_root(document, comm=comm)
+
+    assert isinstance(model, BkTabs)
+    assert len(model.tabs) == 2
+    assert all(isinstance(c, BkPanel) for c in model.tabs)
+    tab1, tab2 = model.tabs
+
+    assert tab1.title == 'Tab1'
+    assert tab1.name == p1.name == 'Div1'
+    assert tab1.child is div1
+    assert tab2.title == 'Tab2'
+    assert tab2.name == p2.name =='Div2'
     assert tab2.child is div2
 
 
@@ -311,15 +400,15 @@ def test_tabs_set_panes(document, comm):
     assert all(isinstance(c, BkPanel) for c in model.tabs)
     tab1, tab2, tab3 = model.tabs
 
-    assert tab1.title == 'Div1'
+    assert tab1.title == tab1.name == p1.name =='Div1'
     assert tab1.child is div1
-    assert tab2.title == 'Div2'
+    assert tab2.title == tab2.name == p2.name =='Div2'
     assert tab2.child is div2
-    assert tab3.title == 'Div3'
+    assert tab3.title == tab3.name == p3.name =='Div3'
     assert tab3.child is div3
 
 
-def test_tabs_append(document, comm):
+def test_tabs_reverse(document, comm):
     div1, div2 = Div(), Div()
     p1 = Pane(div1, name='Div1')
     p2 = Pane(div2, name='Div2')
@@ -327,9 +416,26 @@ def test_tabs_append(document, comm):
 
     model = tabs._get_root(document, comm=comm)
 
+    tabs.reverse()
+    tab1, tab2 = model.tabs
+    assert tab1.child is div2
+    assert tab1.title == tab1.name == p2.name == 'Div2'
+    assert tab2.child is div1
+    assert tab2.title == tab2.name == p1.name == 'Div1'
+
+
+def test_tabs_append(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
     div3 = Div()
     tabs.append(div3)
+
     tab1, tab2, tab3 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div3
 
 
 def test_empty_tabs_append(document, comm):
@@ -343,19 +449,182 @@ def test_empty_tabs_append(document, comm):
     assert model.tabs[0].title == 'test title'
 
 
-def test_tabs_insert(document, comm):
-    div1 = Div()
-    div2 = Div()
-    tabs = Tabs(div1, div2)
+def test_tabs_append_uses_object_name(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3 = Div()
+    p3 = Pane(div3, name='Div3')
+    tabs.append(p3)
+
+    tab1, tab2, tab3 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div3
+    assert tab3.title == tab3.name == p3.name == 'Div3'
+
 
+def test_tabs_append_with_tuple_and_unnamed_contents(document, comm, tabs):
     model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3 = Div()
+    tabs.append(('Div3', div3))
+
+    tab1, tab2, tab3 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div3
+    assert tab3.title == 'Div3'
+
+
+def test_tabs_append_with_tuple_and_named_contents(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3 = Div()
+    p3 = Pane(div3, name='Div3')
+    tabs.append(('Tab3', p3))
+
+    tab1, tab2, tab3 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div3
+    assert tab3.title == 'Tab3'
+    assert tab3.name == p3.name == 'Div3'
+
+
+def test_tabs_extend(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3, div4 = Div(), Div()
+    tabs.extend([div4, div3])
+
+    tab1, tab2, tab3, tab4 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div4
+    assert tab4.child is div3
+
+
+def test_tabs_extend_uses_object_name(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3, div4 = Div(), Div()
+    p3, p4 = Pane(div3, name='Div3'), Pane(div4, name='Div4')
+    tabs.extend([p4, p3])
+
+    tab1, tab2, tab3, tab4 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div4
+    assert tab3.title == p4.name == 'Div4'
+    assert tab4.child is div3
+    assert tab4.title  == p3.name == 'Div3'
+
+
+def test_tabs_extend_with_tuple_and_unnamed_contents(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3, div4 = Div(), Div()
+    tabs.extend([('Div4', div4), ('Div3', div3)])
+
+    tab1, tab2, tab3, tab4 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div4
+    assert tab3.title == 'Div4'
+    assert tab4.child is div3
+    assert tab4.title == 'Div3'
+
+
+def test_tabs_extend_with_tuple_and_named_contents(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3, div4 = Div(), Div()
+    p3, p4 = Pane(div3, name='Div3'), Pane(div4, name='Div4')
+    tabs.extend([('Tab4', p4), ('Tab3', p3)])
+
+    tab1, tab2, tab3, tab4 = model.tabs
+    assert_tab_is_similar(tab1_before, tab1)
+    assert_tab_is_similar(tab2_before, tab2)
+
+    assert tab3.child is div4
+    assert tab3.title == 'Tab4'
+    assert tab3.name == p4.name == 'Div4'
+    assert tab4.child is div3
+    assert tab4.title == 'Tab3'
+    assert tab4.name == p3.name == 'Div3'
+
+
+def test_tabs_insert(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
 
     div3 = Div()
     tabs.insert(1, div3)
+
     tab1, tab2, tab3 = model.tabs
-    assert tab1.child is div1
+
+    assert_tab_is_similar(tab1_before, tab1)
+    assert tab2.child is div3
+    assert_tab_is_similar(tab2_before, tab3)
+
+
+def test_tabs_insert_uses_object_name(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3 = Div()
+    p3 = Pane(div3, name='Div3')
+    tabs.insert(1, p3)
+
+    tab1, tab2, tab3 = model.tabs
+
+    assert_tab_is_similar(tab1_before, tab1)
+    assert tab2.child is div3
+    assert tab2.title == tab2.name == p3.name == 'Div3'
+    assert_tab_is_similar(tab2_before, tab3)
+
+
+def test_tabs_insert_with_tuple_and_unnamed_contents(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3 = Div()
+    tabs.insert(1, ('Div3', div3))
+    tab1, tab2, tab3 = model.tabs
+
+    assert_tab_is_similar(tab1_before, tab1)
+    assert tab2.child is div3
+    assert tab2.title == 'Div3'
+    assert_tab_is_similar(tab2_before, tab3)
+
+
+def test_tabs_insert_with_tuple_and_named_contents(document, comm, tabs):
+    model = tabs._get_root(document, comm=comm)
+    tab1_before, tab2_before = model.tabs
+
+    div3 = Div()
+    p3 = Pane(div3, name='Div3')
+    tabs.insert(1, ('Tab3', p3))
+    tab1, tab2, tab3 = model.tabs
+
+    assert_tab_is_similar(tab1_before, tab1)
     assert tab2.child is div3
-    assert tab3.child is div2
+    assert tab2.title == 'Tab3'
+    assert tab2.name  == p3.name == 'Div3'
+    assert_tab_is_similar(tab2_before, tab3)
 
 
 def test_tabs_setitem(document, comm):
@@ -474,8 +743,8 @@ def test_tabs_setitem_replace_slice_out_of_bounds(document, comm):
     div3 = Div()
     with pytest.raises(IndexError):
         layout[3:4] = [div3]
-        
-    
+
+
 def test_tabs_pop(document, comm):
     div1 = Div()
     div2 = Div()
@@ -514,6 +783,21 @@ def test_tabs_remove(document, comm):
     assert p1._models == {}
 
 
+def test_tabs_clear(document, comm):
+    div1 = Div()
+    div2 = Div()
+    tabs = Tabs(div1, div2)
+    p1, p2 = tabs.objects
+
+    model = tabs._get_root(document, comm=comm)
+
+    tabs.clear()
+    assert tabs._names == []
+    assert len(model.tabs) == 0
+    assert p1._callbacks == p2._callbacks == {}
+    assert p1._models == p2._models == {}
+
+
 def test_spacer(document, comm):
     spacer = Spacer(width=400, height=300)
 
@@ -525,22 +809,22 @@ def test_spacer(document, comm):
 
     spacer.height = 400
     assert model.height == 400
-    
-    
+
+
 def test_layout_with_param_setitem(document, comm):
     import param
     class TestClass(param.Parameterized):
         select = param.ObjectSelector(default=0, objects=[0,1])
-     
+
         def __init__(self, **params):
             super(TestClass, self).__init__(**params)
             self._layout = Row(Param(self.param, parameters=['select']),
                                self.select)
-         
+
         @param.depends('select', watch=True)
         def _load(self):
             self._layout[-1] = self.select
-        
+
     test = TestClass()
     model = test._layout._get_root(document, comm=comm)
     test.select = 1