Repository: bokeh/bokeh

Issue:
Allow layout function to accept arbitrarily nested lists
Currently the layout function only accepts a list of lists.

It would be nice if we could hand it arbitrarily deeply nested lists of things to make it quicker to construct a complex layout.



Discussion:

Resolving patch:
diff --git a/bokeh/layouts.py b/bokeh/layouts.py
--- a/bokeh/layouts.py
+++ b/bokeh/layouts.py
@@ -248,21 +248,28 @@ def layout(*args, **kwargs):
     children = _handle_children(*args, children=children)
 
     # Make the grid
-    rows = []
-    for r in children:
-        row_children = []
-        for item in r:
-            if isinstance(item, LayoutDOM):
-                item.sizing_mode = sizing_mode
-                row_children.append(item)
-            else:
-                raise ValueError(
-                    """Only LayoutDOM items can be inserted into a layout.
-                    Tried to insert: %s of type %s""" % (item, type(item))
-                )
-        rows.append(row(children=row_children, sizing_mode=sizing_mode))
-    grid = column(children=rows, sizing_mode=sizing_mode)
-    return grid
+    return _create_grid(children, sizing_mode)
+
+
+def _create_grid(iterable, sizing_mode, layer=0):
+    """Recursively create grid from input lists."""
+    return_list = []
+    for item in iterable:
+        if isinstance(item, list):
+            return_list.append(_create_grid(item, sizing_mode, layer+1))
+        elif isinstance(item, LayoutDOM):
+            item.sizing_mode = sizing_mode
+            return_list.append(item)
+        else:
+            raise ValueError(
+                """Only LayoutDOM items can be inserted into a layout.
+                Tried to insert: %s of type %s""" % (item, type(item))
+            )
+    if layer % 2 == 0:
+        return column(children=return_list, sizing_mode=sizing_mode)
+    return row(children=return_list, sizing_mode=sizing_mode)
+
+    return return_list
 
 
 def _chunks(l, ncols):


Test patch:
diff --git a/bokeh/tests/test_layouts.py b/bokeh/tests/test_layouts.py
--- a/bokeh/tests/test_layouts.py
+++ b/bokeh/tests/test_layouts.py
@@ -1,6 +1,8 @@
+import bokeh.layouts as lyt
+import pytest
+from bokeh.core.enums import SizingMode
 from bokeh.plotting import figure
 
-import bokeh.layouts as lyt
 
 def test_gridplot_merge_tools_flat():
     p1, p2, p3, p4 = figure(), figure(), figure(), figure()
@@ -10,6 +12,7 @@ def test_gridplot_merge_tools_flat():
     for p in p1, p2, p3, p4:
         assert p.toolbar_location is None
 
+
 def test_gridplot_merge_tools_with_None():
     p1, p2, p3, p4 = figure(), figure(), figure(), figure()
 
@@ -29,3 +32,35 @@ def test_gridplot_merge_tools_nested():
 
     for p in p1, p2, p3, p4, p5, p6, p7:
         assert p.toolbar_location is None
+
+
+def test_layout_simple():
+    p1, p2, p3, p4 = figure(), figure(), figure(), figure()
+
+    grid = lyt.layout([[p1, p2], [p3, p4]], sizing_mode='fixed')
+
+    assert isinstance(grid, lyt.Column)
+    for row in grid.children:
+        assert isinstance(row, lyt.Row)
+
+
+def test_layout_nested():
+    p1, p2, p3, p4, p5, p6 = figure(), figure(), figure(), figure(), figure(), figure()
+
+    grid = lyt.layout([[[p1, p1], [p2, p2]], [[p3, p4], [p5, p6]]], sizing_mode='fixed')
+
+    assert isinstance(grid, lyt.Column)
+    for row in grid.children:
+        assert isinstance(row, lyt.Row)
+        for col in row.children:
+            assert isinstance(col, lyt.Column)
+
+
+@pytest.mark.parametrize('sizing_mode', SizingMode)
+def test_layout_sizing_mode(sizing_mode):
+    p1, p2, p3, p4 = figure(), figure(), figure(), figure()
+
+    lyt.layout([[p1, p2], [p3, p4]], sizing_mode=sizing_mode)
+
+    for p in p1, p2, p3, p4:
+        assert p1.sizing_mode == sizing_mode