Repository: bokeh/bokeh

Issue:
Is there any reason why p.select() shouldn't take **kwargs?
Looking at https://github.com/bokeh/bokeh/blob/master/bokeh/models/plots.py#L58, it should be a fairly clean change to convert **kwargs into JSON before passing it into the `find()` call...  This would really make the interface a lot cleaner.

The only downside I can see is that in the future, the `.select()` method could not safely grow any actual keyword arguments, lest they conflict with attribute names which are being searched for...



Discussion:
Actually, while we're at it...

Why not change it to also accept positional arguments, i.e. if a string is passed in, it's assumed to be "name=", if a Python `type` is passed in and it's a subclass of PlotObject or HasProps or something, then it's assumed to be a query of the form "type=".  This leads to a must more natural usage, like:

```
p.select(TapTool)
```

and 

```
p.select("outliers")  # for a scatter renderer named "outliers"
```

Thoughts?

I like the idea of positional arguments, specially regarding the named renderers.. OTOH as someone new to the codebase and concepts I think that positional arguments that change "meaning" depending on their type can easily be very confusing.

Honestly I already find `.select` a bit confusing as we should better document what keys and values `selector` can specify ( right now we only have:  `selector (JSON-like) :`). If we do add this bit of info to the docs I'd be +10 for the *\* magic. I find it more elegant and does not force the user to pass everything into a dict..

For that reason I think my preference would go for just the **kwargs

Love this.

Propose:

```
    # enforce len(kwargs) == 0, len(args) == 1
    p.select("a string")                    # p.select({"name:" "a string"})
    p.select(PropsType)                     # p.select({"type": PropsType})
    p.select({"foo": "bar", "baz": "quux"}) # p.select({"foo": "bar", "baz": "quux"})

    # enforce len(args) = 0
    p.select(foo="bar", baz="quux")         # p.select({"foo": "bar", "baz": "quux"})
```

@Maggie-M I'd like you to look at this.

Bryan, surely your third line was meant to read:

```
p.select(foo="bar", baz="quux")
```

?

No? The third line is the current behaviour (pass in a single selector dict as the arg) and I was just being explicit that we should also keep that. Unless I am missing something?

@fpliger You're right, we need to document the types that can be searched for and provide a bunch of examples of common things.

I ran into this issue while writing the examples last night in response to the mailing list question, and realized that `select()` was the recommended way to get the renderer after using `p.glyphfunc(...)` to create circles and lines.

Ah OK.  Yes, in the case of an actual dict, it needs to pass through.  I see now - I was misunderstanding the comments somehow.

Yes, sorry the comments are the "translations" into the equivalent code with the current interface

So the easiest thing is probably to rename `Plot.select` to `Plot._select` and then make a new `Plot.select` that just does all the argument handling/checking and calls the original. 

Yep, great way to do it.


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,20 +3,23 @@
 """
 from __future__ import absolute_import
 
+from six import string_types
+
+from ..enums import Location
+from ..mixins import LineProps, TextProps
 from ..plot_object import PlotObject
 from ..properties import Bool, Int, String, Color, Enum, Auto, Instance, Either, List, Dict, Include
-from ..mixins import LineProps, TextProps
-from .. enums import Location
-
 from ..query import find
 from ..util.string import nice_join
 
-from .widget import Widget
-from .sources import DataSource, ColumnDataSource
+
+from .glyphs import Glyph
 from .ranges import Range, Range1d
 from .renderers import Renderer, GlyphRenderer
+from .sources import DataSource, ColumnDataSource
 from .tools import Tool, ToolEvents
-from .glyphs import Glyph
+from .widget import Widget
+
 
 # TODO (bev) dupe, move to utils
 class _list_attr_splat(list):
@@ -55,17 +58,77 @@ def __init__(self, **kwargs):
             kwargs["tool_events"] = ToolEvents()
         super(Plot, self).__init__(**kwargs)
 
-    def select(self, selector):
+    def select(self, *args, **kwargs):
         ''' Query this object and all of its references for objects that
         match the given selector.
 
+        There are a few different ways to call the ``select`` method.
+        The most general is to supply a JSON-like query dictionary as the
+        single argument or as keyword arguments:
+
         Args:
-            selector (JSON-like) :
+            selector (JSON-like) : some sample text
+
+        Keyword Arguments:
+            kwargs : query dict key/values as keyword arguments
+
+        For convenience, queries on just names can be made by supplying
+        the ``name`` string as the single parameter:
+
+        Args:
+            name (str) : the name to query on
+
+        Also queries on just type can be made simply by supplying the
+        ``PlotObject`` subclass as the single parameter:
+
+        Args:
+            type (PlotObject) : the type to query on
 
         Returns:
             seq[PlotObject]
 
+        Examples:
+
+            .. code-block:: python
+
+                # These two are equivalent
+                p.select({"type": HoverTool})
+                p.select(HoverTool)
+
+                # These two are also equivalent
+                p.select({"name": "mycircle"})
+                p.select("mycircle")
+
+                # Keyword arguments can be supplied in place of selector dict
+                p.select({"name": "foo", "type": HoverTool})
+                p.select(name="foo", type=HoverTool)
+
         '''
+
+        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, PlotObject):
+                selector = {"type" : arg}
+            else:
+                raise RuntimeError("Selector must be a dictionary, string or plot object.")
+
+        else:
+            selector = kwargs
+
+        # Want to pass selector that is a dictionary
         return _list_attr_splat(find(self.references(), selector, {'plot': self}))
 
     def row(self, row, gridplot):
@@ -151,6 +214,7 @@ def add_glyph(self, source_or_glyph, glyph=None, **kw):
             source (DataSource) : a data source for the glyphs to all use
             glyph (Glyph) : the glyph to add to the Plot
 
+
         Keyword Arguments:
             Any additional keyword arguments are passed on as-is to the
             Glyph initializer.
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -450,6 +450,7 @@ def parse_jsargs():
     packages=[
         'bokeh',
         'bokeh.models',
+        'bokeh.models.tests',
         'bokeh.models.widgets',
         'bokeh.charts',
         'bokeh.charts.builder',


Test patch:
diff --git a/bokeh/models/tests/__init__.py b/bokeh/models/tests/__init__.py
new file mode 100644
diff --git a/bokeh/models/tests/test_plots.py b/bokeh/models/tests/test_plots.py
new file mode 100644
--- /dev/null
+++ b/bokeh/models/tests/test_plots.py
@@ -0,0 +1,73 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) 2012 - 2015, Continuum Analytics, Inc. All rights reserved.
+#
+# Powered by the Bokeh Development Team.
+#
+# The full license is in the file LICENSE.txt, distributed with this software.
+#-----------------------------------------------------------------------------
+
+from __future__ import absolute_import
+
+from mock import patch
+import unittest
+
+from bokeh.plotting import figure
+from bokeh.models import GlyphRenderer
+from bokeh.models.tools import HoverTool, PanTool
+
+import bokeh.models.plots as plots
+
+class TestPlotSelect(unittest.TestCase):
+
+    def setUp(self):
+        self._plot = figure(tools='pan')
+        self._plot.circle([1,2,3], [3,2,1], name='foo')
+
+    @patch('bokeh.models.plots.find')
+    def test_string_arg(self, mock_find):
+        self._plot.select('foo')
+        self.assertTrue(mock_find.called)
+        self.assertEqual(mock_find.call_args[0][1], dict(name='foo'))
+
+    @patch('bokeh.models.plots.find')
+    def test_type_arg(self, mock_find):
+        self._plot.select(PanTool)
+        self.assertTrue(mock_find.called)
+        self.assertEqual(mock_find.call_args[0][1], dict(type=PanTool))
+
+    @patch('bokeh.models.plots.find')
+    def test_kwargs(self, mock_find):
+        kw = dict(name='foo', type=GlyphRenderer)
+        self._plot.select(**kw)
+        self.assertTrue(mock_find.called)
+        self.assertEqual(mock_find.call_args[0][1], kw)
+
+    def test_too_many_args(self):
+        with self.assertRaises(TypeError) as cm:
+             self._plot.select('foo', 'bar')
+        self.assertEqual(
+            'select accepts at most ONE positional argument.',
+            str(cm.exception)
+        )
+
+    def test_no_input(self):
+        with self.assertRaises(TypeError) as cm:
+             self._plot.select()
+        self.assertEqual(
+            'select requires EITHER a positional argument, OR keyword arguments.',
+            str(cm.exception)
+        )
+
+    def test_arg_and_kwarg(self):
+        with self.assertRaises(TypeError) as cm:
+             self._plot.select('foo', type=PanTool)
+        self.assertEqual(
+            'select accepts EITHER a positional argument, OR keyword arguments (not both).',
+            str(cm.exception)
+        )
+
+
+
+
+if __name__ == '__main__':
+    unittest.main()