Repository: bokeh/bokeh

Issue:
Don't warn on empty layouts
#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)
```
Python version      :  3.6.7 | packaged by conda-forge | (default, Nov 20 2018, 18:37:09)
IPython version     :  7.2.0
Tornado version     :  5.1.1
Bokeh version       :  1.1.0dev9+2.g1138433
BokehJS static path :  /Users/jsignell/conda/envs/bokeh-dev/lib/python3.6/site-packages/bokeh-1.1.0.dev9+2.g1138433-py3.6.egg/bokeh/server/static
node.js version     :  v11.6.0
npm version         :  6.5.0
```

#### Description of expected behavior and the observed behavior
I think that layouts should be allowed to be empty without raising a warning. There is a legitimate use of an empty layout object to create an empty area which can later be appended to or extended. This is especially useful in panel (see issue: https://github.com/pyviz/panel/issues/285) where the layout object will re-render if the children change. 

I understand that it could potentially be confusing if someone were to create an empty layout object and then think that it is just not rendering - but it doesn't seem like that would be terribly common (if it has been in the past, then maybe this warning shouldn't be removed).

#### Complete, minimal, self-contained example code that reproduces the issue

```python
from bokeh.io import output_notebook, show
from bokeh.layouts import column
from bokeh.models.widgets import Button

output_notebook()

col = column([])
show(col)
```
warning will be displayed

```python
# add a button
button = Button(label='Click me!')
col.children.append(button)
show(col)
```
column will render properly

#### Stack traceback and/or browser JavaScript console output

This is the relevant warning:

https://github.com/bokeh/bokeh/blob/113843391ae24a685c12f04bc57cb2404df56155/bokeh/core/validation/warnings.py#L13-L14

#### Screenshots or screencasts of the bug in action

This is the warning:

<img width="820" alt="screen shot 2019-03-07 at 8 57 03 am" src="https://user-images.githubusercontent.com/4806877/53962228-86214c00-40b8-11e9-94bd-c9e1cff00eb9.png">

And this is the column rendering properly after adding an item to it:
<img width="817" alt="screen shot 2019-03-07 at 8 58 31 am" src="https://user-images.githubusercontent.com/4806877/53962235-891c3c80-40b8-11e9-8459-0c958a96784f.png">





Discussion:
Personally, I think that empty containers should almost never be considered an error condition.  They come up in many legitimate situations, such as dynamically constructing plots based on a query or external file, as temporaries that will later have things put in them, as placeholders for items that may or may not turn out to be available, as the limiting case to various algorithms, and so on.  I can imagine some cases where this warning would be helpful to a user, but there are so many cases where it is inappropriate and forces users into awkward approaches to avoid the warning.  So I vote for disabling the warning by default.
This is something that was carried over from the old layout. I will remove this warning.
Well, my original intent was to make various warnings individually on/off configurable so then things like HV could turn them off if that made sense. e.g.
```
bokeh.core.validation.silence(EMPTY_LAYOUT, True)
```
or similar, but this has not happened as of yet. 

FWIW this warning was specifically added in response to a bunch of "why is my page empty?" support questions, to try and give users immediate actionable feedback that would save them from making an issue or ML/SO question. So removing it entirely is asking us to assume the burden of explicitly handling those reports again. I'd prefer, if we are to spend effort, to add the configuration  capability. That would let HV disable the ones that don't make sense for its usage. 

> such as dynamically constructing plots based on a query or external file, as temporaries that will later have things put in them, as placeholders for items that may or may not turn out to be available, as the limiting case to various algorithms, and so on.

Just to make my position clear, these are all advanced or relatively rare use cases, relative to most normal usage. I think they should be *possible* (i.e. a configuration to turn off the pesky warning). But I think default operation should optimize for helping out with common usage, which in my judgment is "you probably didn't mean to show an empty layout" (we could also tweak the warning message and include instructions right there for how to silence it as well).

I can't quite see what a user would have expected to happen for an empty layout other than having an empty page; what were they hoping for?  And I don't get much out of "Layout has no children".  So I'll have to defer to your judgment and painful experience with that. :-)
> I can't quite see what a user would have expected to happen for an empty layout other than having an empty page

That is the point: they were not *intending* to show empty layouts, they just had happened to, by some mistake, but did not realize it. 
Since you guys have experienced pain from this before then I think it makes perfect sense to just make the disabling of warnings easily configurable. That is a more general solution anyways since there will likely be other situations in which what is right for bokeh users isn't necessarily right for downstream projects.
@jsignell do you want to make a PR? I think it should be fairly straightforward, e.g. just an internal set of warning numbers to silence (added/remove via some API) that gets checked before a warning is emitted. 
Sure I will take a stab at it. 
I will have to disagree, because warning about something that's perfectly valid or even encouraged in the new layout (grids with track specs and no children), is wrong in my opinion. It's even worse to make it configurable, but on by default. This will encourage users to just disable warnings altogether and risk not being informed about something important (actually wrong, but not critical). I would reverse the logic and let users know by default only about actual problems, and provide them with a mode in which any potential issues would be pointed out and explained. This way users who know what they are doing wouldn't be annoyed and others would still get the help needed to figure out any issues. This could actually solve some of bokeh's performance issues, because we could do less validation by default, but allow very robust validation upon request. Anyway, that's a significant chunk of work, so I suppose it's outside the scope of this issue.
That's an interesting idea -- you could have the Bokeh issue template suggest that users turn on this special verbose mode before they report an issue, because the messages may help them discover the problem themselves.
The crux of the matter is that I strongly suspect that the number of people legitimately intending to show an empty layout is minuscule [1] compared to a much greater occurrence of people unintentionally showing an empty layout by mistake, left wondering why they are staring at an empty screen with no guidance or feedback. In which case we are optimizing for the wrong (uncommon) case.

[1] Case in point, it is basically never useful to show an empty layout for any standalone, non-server case, because there is no way to update and add anything after the fact. Most usage ("standard" notebook usage, HTML file output, `components` or `json_items` embedding) is non-server. 

Edit: then perhaps there is the compromise: we don't warn in the context of server apps, but we do warn always for other usage. 

Resolving patch:
diff --git a/bokeh/colors/util.py b/bokeh/colors/util.py
--- a/bokeh/colors/util.py
+++ b/bokeh/colors/util.py
@@ -74,7 +74,7 @@ def __iter__(self):
 
     def __getattr__(self, v):
         from . import named
-        if v is not "_colors" and v in self._colors:
+        if v != "_colors" and v in self._colors:
             return getattr(named, v.lower())
         return super(_ColorGroupMeta, self).__getattr__(v)
 
diff --git a/bokeh/core/validation/__init__.py b/bokeh/core/validation/__init__.py
--- a/bokeh/core/validation/__init__.py
+++ b/bokeh/core/validation/__init__.py
@@ -22,6 +22,18 @@
 when outputting a visualization for display. This module contains error and
 warning codes as well as helper functions for defining validation checks.
 
+One use case for warnings is to loudly point users in the right direction
+when they accidentally do something that they probably didn't mean to do - this
+is the case for EMPTY_LAYOUT for instance. Since warnings don't necessarily
+indicate misuse, they are configurable. To silence a warning, use the silence
+function provided.
+
+.. code-block:: python
+
+    >>> from bokeh.core.validation import silence
+    >>> from bokeh.core.validation.warnings import EMPTY_LAYOUT
+    >>> silence(EMPTY_LAYOUT, True)
+
 '''
 
 #-----------------------------------------------------------------------------
@@ -41,7 +53,7 @@
 # External imports
 
 # Bokeh imports
-from .check import check_integrity
+from .check import check_integrity, silence
 from .decorators import error, warning
 
 #-----------------------------------------------------------------------------
diff --git a/bokeh/core/validation/check.py b/bokeh/core/validation/check.py
--- a/bokeh/core/validation/check.py
+++ b/bokeh/core/validation/check.py
@@ -29,15 +29,56 @@
 #-----------------------------------------------------------------------------
 # Globals and constants
 #-----------------------------------------------------------------------------
+__silencers__ = set()
 
 __all__ = (
     'check_integrity',
+    'silence'
 )
 
 #-----------------------------------------------------------------------------
 # General API
 #-----------------------------------------------------------------------------
 
+def silence(warning, silence=True):
+    ''' Silence a particular warning on all Bokeh models.
+
+    Args:
+        warning (Warning) : Bokeh warning to silence
+        silence (bool) : Whether or not to silence the warning
+
+    Returns:
+        A set containing the all silenced warnings
+
+    This function adds or removes warnings from a set of silencers which
+    is refered to when running ``check_integrity``. If a warning
+    is added to the silencers - then it will never be raised.
+
+    .. code-block:: python
+
+        >>> from bokeh.core.validation.warnings import EMPTY_LAYOUT
+        >>> bokeh.core.validation.silence(EMPTY_LAYOUT, True)
+        {1002}
+
+    To turn a warning back on use the same method but with the silence
+    argument set to false
+
+    .. code-block:: python
+
+        >>> bokeh.core.validation.silence(EMPTY_LAYOUT, False)
+        set()
+
+    '''
+    if not isinstance(warning, int):
+        raise ValueError('Input to silence should be a warning object '
+                         '- not of type {}'.format(type(warning)))
+    if silence:
+        __silencers__.add(warning)
+    elif warning in __silencers__:
+        __silencers__.remove(warning)
+    return __silencers__
+
+
 def check_integrity(models):
     ''' Apply validation and integrity checks to a collection of Bokeh models.
 
@@ -75,7 +116,9 @@ def check_integrity(models):
         log.error("E-%d (%s): %s: %s" % msg)
 
     for msg in sorted(messages['warning']):
-        log.warning("W-%d (%s): %s: %s" % msg)
+        code, name, desc, obj = msg
+        if code not in __silencers__:
+            log.warning("W-%d (%s): %s: %s" % msg)
 
     # This will be turned on in a future release
     # if len(messages['error']) or (len(messages['warning']) and settings.strict()):


Test patch:
diff --git a/bokeh/core/tests/test_validation.py b/bokeh/core/tests/test_validation.py
--- a/bokeh/core/tests/test_validation.py
+++ b/bokeh/core/tests/test_validation.py
@@ -126,6 +126,70 @@ def test_check_warn(mock_warn, mock_error):
     assert not mock_error.called
     assert mock_warn.called
 
+@patch('bokeh.core.validation.check.log.error')
+@patch('bokeh.core.validation.check.log.warning')
+def test_silence_and_check_warn(mock_warn, mock_error):
+    from bokeh.core.validation.warnings import EXT
+    m = Mod(foo=-10)
+    try:
+        v.silence(EXT)  # turn the warning off
+        v.check_integrity([m])
+        assert not mock_error.called
+        assert not mock_warn.called
+    finally:
+        v.silence(EXT, False)  # turn the warning back on
+        v.check_integrity([m])
+        assert not mock_error.called
+        assert mock_warn.called
+
+@patch('bokeh.core.validation.check.log.error')
+@patch('bokeh.core.validation.check.log.warning')
+def test_silence_with_bad_input_and_check_warn(mock_warn, mock_error):
+    m = Mod(foo=-10)
+    with pytest.raises(ValueError, match=('Input to silence should be a '
+                                          'warning object')):
+        v.silence('EXT:W')
+    v.check_integrity([m])
+    assert not mock_error.called
+    assert mock_warn.called
+
+@patch('bokeh.core.validation.check.log.error')
+@patch('bokeh.core.validation.check.log.warning')
+def test_silence_warning_already_in_silencers_is_ok(mock_warn, mock_error):
+    from bokeh.core.validation.warnings import EXT
+    m = Mod(foo=-10)
+    try:
+        silencers0= v.silence(EXT)  # turn the warning off
+        silencers1 = v.silence(EXT)  # do it a second time - no-op
+        assert len(silencers0) == 1
+        assert silencers0 == silencers1  # silencers is same as before
+
+        v.check_integrity([m])
+        assert not mock_error.called
+        assert not mock_warn.called
+    finally:
+        v.silence(EXT, False)  # turn the warning back on
+        v.check_integrity([m])
+        assert not mock_error.called
+        assert mock_warn.called
+
+@patch('bokeh.core.validation.check.log.error')
+@patch('bokeh.core.validation.check.log.warning')
+def test_silence_remove_warning_that_is_not_in_silencers_is_ok(mock_warn, mock_error):
+    from bokeh.core.validation.warnings import EXT
+    m = Mod(foo=-10)
+
+    silencers0 = v.silence(EXT)  # turn the warning off
+    assert len(silencers0) == 1
+
+    silencers1 = v.silence(EXT, False)  # turn the warning back on
+    silencers2 = v.silence(EXT, False)  # do it a second time - no-op
+    assert len(silencers1) == 0
+    assert silencers1 == silencers2
+
+    v.check_integrity([m])
+    assert not mock_error.called
+    assert mock_warn.called
 #-----------------------------------------------------------------------------
 # Private API
 #-----------------------------------------------------------------------------
diff --git a/bokeh/models/tests/test_callbacks.py b/bokeh/models/tests/test_callbacks.py
--- a/bokeh/models/tests/test_callbacks.py
+++ b/bokeh/models/tests/test_callbacks.py
@@ -44,7 +44,7 @@ def test_js_callback():
 
     cb = CustomJS(code="foo();", args=dict(x=3))
     assert 'foo()' in cb.code
-    assert cb.args['x'] is 3
+    assert cb.args['x'] == 3
 
     with raises(AttributeError):  # kwargs not supported
         CustomJS(code="foo();", x=slider)
@@ -64,7 +64,7 @@ def cb(x=4):
         foo()
     cb = CustomJS.from_py_func(cb)
     assert 'foo()' in cb.code
-    assert cb.args['x'] is 4
+    assert cb.args['x'] == 4
 
 #-----------------------------------------------------------------------------
 # Dev API
diff --git a/bokeh/tests/test_transform.py b/bokeh/tests/test_transform.py
--- a/bokeh/tests/test_transform.py
+++ b/bokeh/tests/test_transform.py
@@ -101,7 +101,7 @@ def test_basic(self):
         assert t['transform'].palette == ["red", "green"]
         assert t['transform'].factors == ["foo", "bar"]
         assert t['transform'].start == 1
-        assert t['transform'].end is 2
+        assert t['transform'].end == 2
         assert t['transform'].nan_color == "pink"
 
     def test_defaults(self):