Repository: bokeh/bokeh

Issue:
If the "selector" argument is explicitily set to the "select" method, an empty list is returned
A small bug.

If I write manually the argument name `selector` using the `select` method, the returned valueis an empty list, however the `select_one` works well.

    from bokeh.io import output_notebook, show, push_notebook
    output_notebook()

    from bokeh.models import ColumnDataSource, CustomJS, IndexFilter, CDSView
    from bokeh.plotting import figure

    source = ColumnDataSource(dict(
        x=[1, 2, 3, 4, 5, 6],
        y=[1, 2, 3, 4, 5, 6],
    ))

    p = figure(
        plot_height=300,
        tools='lasso_select'
    )

    rc = p.circle(
        x='x',
        y='y',
        size=20,
        source=source,
        fill_alpha=1.0,
        line_alpha=1.0,
    )
    rc.tags = ['GR', 'GRS']

    rs = p.select(dict(tags=['GR']))
    rs = p.select_one(dict(tags=['GR']))

    rs = p.select(selector=dict(tags=['GR']))         # returns an empty list
    rs = p.select_one(selector=dict(tags=['GR']))

These are the [code declarations](https://bokeh.pydata.org/en/latest/docs/reference/model.html#bokeh.model.Model.select)

    def select(self, selector):
        ''' Query this object and all of its references for objects that
        match the given selector.

        Args:
            selector (JSON-like) :

        Returns:
            seq[Model]

        '''
        return find(self.references(), selector)


    def select_one(self, selector):
        ''' Query this object and all of its references for objects that
        match the given selector.  Raises an error if more than one object
        is found.  Returns single matching object, or None if nothing is found
        Args:
            selector (JSON-like) :

        Returns:
            Model
        '''
        result = list(self.select(selector))
        if len(result) > 1:
            raise ValueError("Found more than one object matching %s: %r" % (selector, result))
        if len(result) == 0:
            return None
        return result[0]


Discussion:
The problem is that `Plot` overrides the `select` defined on `Model` in order to add some metadata needed for some cases specific to searching plots. But it doesn't respect a `selector` kwarg (it should):
````
In [3]: len(p.select(dict(tags=['GR'])))
{'tags': ['GR']}
Out[3]: 1

In [4]: len(p.select(selector=dict(tags=['GR'])))
{'selector': {'tags': ['GR']}}
Out[4]: 0
```

This function needs to be updated to look for a kwarg named "selector" explicitly: 

https://github.com/bokeh/bokeh/blob/master/bokeh/util/plot_utils.py#L18

Resolving patch:
diff --git a/bokeh/models/plots.py b/bokeh/models/plots.py
--- a/bokeh/models/plots.py
+++ b/bokeh/models/plots.py
@@ -3,6 +3,8 @@
 '''
 from __future__ import absolute_import
 
+from six import string_types
+
 from ..core.enums import Location, OutputBackend
 from ..core.properties import Bool, Dict, Enum, Include, Instance, Int, List, Override, String, Float
 from ..core.property_mixins import LineProps, FillProps
@@ -10,7 +12,7 @@
 from ..core.validation import error, warning
 from ..core.validation.errors import REQUIRED_RANGE, REQUIRED_SCALE, INCOMPATIBLE_SCALE_AND_RANGE
 from ..core.validation.warnings import MISSING_RENDERERS
-from ..util.plot_utils import _list_attr_splat, _select_helper
+from ..model import Model
 from ..util.string import nice_join
 
 from .annotations import Legend, Title
@@ -28,6 +30,56 @@ def _check_conflicting_kwargs(a1, a2, kwargs):
     if a1 in kwargs and a2 in kwargs:
         raise ValueError("Conflicting properties set on plot: %r and %r" % (a1, a2))
 
+class _list_attr_splat(list):
+    def __setattr__(self, attr, value):
+        for x in self:
+            setattr(x, attr, value)
+
+    def __dir__(self):
+        if len(set(type(x) for x in self)) == 1:
+            return dir(self[0])
+        else:
+            return dir(self)
+
+
+def _select_helper(args, kwargs):
+    """ Allow flexible selector syntax.
+
+    Returns:
+        dict
+
+    """
+    if len(args) > 1:
+        raise TypeError("select accepts at most ONE positional argument.")
+
+    if len(args) > 0 and len(kwargs) > 0:
+        raise TypeError("select accepts EITHER a positional argument, OR keyword arguments (not both).")
+
+    if len(args) == 0 and len(kwargs) == 0:
+        raise TypeError("select requires EITHER a positional argument, OR keyword arguments.")
+
+    if args:
+        arg = args[0]
+        if isinstance(arg, dict):
+            selector = arg
+        elif isinstance(arg, string_types):
+            selector = dict(name=arg)
+        elif isinstance(arg, type) and issubclass(arg, Model):
+            selector = {"type": arg}
+        else:
+            raise TypeError("selector must be a dictionary, string or plot object.")
+
+    elif 'selector' in kwargs:
+        if len(kwargs) == 1:
+            selector = kwargs['selector']
+        else:
+            raise TypeError("when passing 'selector' keyword arg, not other keyword args may be present")
+
+    else:
+        selector = kwargs
+
+    return selector
+
 class Plot(LayoutDOM):
     ''' Model representing a plot, containing glyphs, guides, annotations.
 
@@ -61,6 +113,10 @@ def select(self, *args, **kwargs):
         Keyword Arguments:
             kwargs : query dict key/values as keyword arguments
 
+        Additionally, for compatibility with ``Model.select``, a selector
+        dict may be passed as ``selector`` keyword argument, in which case
+        the value of ``kwargs['selector']`` is used for th query.
+
         For convenience, queries on just names can be made by supplying
         the ``name`` string as the single parameter:
 
@@ -80,7 +136,8 @@ def select(self, *args, **kwargs):
 
             .. code-block:: python
 
-                # These two are equivalent
+                # These three are equivalent
+                p.select(selector={"type": HoverTool})
                 p.select({"type": HoverTool})
                 p.select(HoverTool)
 
diff --git a/bokeh/util/__init__.py b/bokeh/util/__init__.py
--- a/bokeh/util/__init__.py
+++ b/bokeh/util/__init__.py
@@ -78,14 +78,6 @@
 .. automodule:: bokeh.util.paths
   :members:
 
-.. _bokeh.util.plot_utils:
-
-``bokeh.util.plot_utils``
--------------------------
-
-.. automodule:: bokeh.util.plot_utils
-  :members:
-
 .. _bokeh.util.serialization:
 
 ``bokeh.util.serialization``
diff --git a/bokeh/util/plot_utils.py b/bokeh/util/plot_utils.py
deleted file mode 100644
--- a/bokeh/util/plot_utils.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from six import string_types
-
-from ..model import Model
-
-
-class _list_attr_splat(list):
-    def __setattr__(self, attr, value):
-        for x in self:
-            setattr(x, attr, value)
-
-    def __dir__(self):
-        if len(set(type(x) for x in self)) == 1:
-            return dir(self[0])
-        else:
-            return dir(self)
-
-
-def _select_helper(args, kwargs):
-    """
-    Allow flexible selector syntax.
-    Returns:
-        a dict
-    """
-    if len(args) > 1:
-        raise TypeError("select accepts at most ONE positional argument.")
-
-    if len(args) > 0 and len(kwargs) > 0:
-        raise TypeError("select accepts EITHER a positional argument, OR keyword arguments (not both).")
-
-    if len(args) == 0 and len(kwargs) == 0:
-        raise TypeError("select requires EITHER a positional argument, OR keyword arguments.")
-
-    if args:
-        arg = args[0]
-        if isinstance(arg, dict):
-            selector = arg
-        elif isinstance(arg, string_types):
-            selector = dict(name=arg)
-        elif issubclass(arg, Model):
-            selector = {"type": arg}
-        else:
-            raise RuntimeError("Selector must be a dictionary, string or plot object.")
-
-    else:
-        selector = kwargs
-    return selector


Test patch:
diff --git a/bokeh/models/tests/test_plots.py b/bokeh/models/tests/test_plots.py
--- a/bokeh/models/tests/test_plots.py
+++ b/bokeh/models/tests/test_plots.py
@@ -43,20 +43,37 @@ def test_kwargs(self, mock_find):
         assert mock_find.called
         assert mock_find.call_args[0][1] == kw
 
+    @patch('bokeh.models.plots.find')
+    def test_single_selector_kwarg(self, mock_find):
+        kw = dict(name='foo', type=GlyphRenderer)
+        self._plot.select(selector=kw)
+        assert mock_find.called
+        assert mock_find.call_args[0][1] == kw
+
+    def test_selector_kwarg_and_extra_kwargs(self):
+        with pytest.raises(TypeError) as exc:
+            self._plot.select(selector=dict(foo='foo'), bar='bar')
+        assert "when passing 'selector' keyword arg, not other keyword args may be present" == str(exc.value)
+
+    def test_bad_arg_type(self):
+        with pytest.raises(TypeError) as exc:
+            self._plot.select(10)
+        assert "selector must be a dictionary, string or plot object." == str(exc.value)
+
     def test_too_many_args(self):
-        with pytest.raises(TypeError) as cm:
+        with pytest.raises(TypeError) as exc:
             self._plot.select('foo', 'bar')
-            assert 'select accepts at most ONE positional argument.' == str(cm.exception)
+        assert 'select accepts at most ONE positional argument.' == str(exc.value)
 
     def test_no_input(self):
-        with pytest.raises(TypeError) as cm:
+        with pytest.raises(TypeError) as exc:
             self._plot.select()
-            assert 'select requires EITHER a positional argument, OR keyword arguments.' == str(cm.exception)
+        assert 'select requires EITHER a positional argument, OR keyword arguments.' == str(exc.value)
 
     def test_arg_and_kwarg(self):
-        with pytest.raises(TypeError) as cm:
+        with pytest.raises(TypeError) as exc:
             self._plot.select('foo', type=PanTool)
-            assert 'select accepts EITHER a positional argument, OR keyword arguments (not both).' == str(cm.exception)
+        assert 'select accepts EITHER a positional argument, OR keyword arguments (not both).' == str(exc.value)
 
 
 def test_plot_add_layout_raises_error_if_not_render():