Repository: bokeh/bokeh

Issue:
[BUG] "set_select" and "select" are not working well with layouts
With the rest of the elements: `Circles`, `LinearAxis`, `Title` the method `set_select` works well as you can check in the following example. But, I am not able to make it work when I try to select plots through the layout:

    from bokeh.io import show
    from bokeh.models import ColumnDataSource, LinearAxis, Title, Circle, Plot
    from bokeh.plotting import figure, Figure
    from bokeh.layouts import column, row

    source = ColumnDataSource(dict(
        x=[1, 2, 3, 4, 5, 6],
        y=[1, 2, 3, 4, 5, 6],
    ))

    # ------------------ PLOT 1 ---------------------- #

    p = figure(
        title='Original',
        plot_width=200,
        plot_height=200,
        x_axis_label='X Axis',
        y_axis_label='Y Axis',
        tools='lasso_select,tap',
    )
    p.circle(
        x='x', y='y',
        size=3, source=source
    )
    p.xaxis.axis_label_text_font_style = 'normal'
    p.yaxis.axis_label_text_font_style = 'normal'

    # ------------------ BIGGER PLOT ---------------------- #

    p2 = figure(
        name='bigger',
        title='Bigger',
        plot_width=200*3,
        plot_height=200*3,
        x_axis_label='X Axis',
        y_axis_label='Y Axis',
        tools='',
        toolbar_location=None,
    )
    c2 = p2.circle(
        name='p2_circle',
        x='x', y='y',
        size=3, source=source
    )
    p2.xaxis.axis_label_text_font_style = 'normal'
    p2.yaxis.axis_label_text_font_style = 'normal'

    big_width = 2

    p2.set_select(
        selector=LinearAxis,
        updates=dict(
            axis_line_width=big_width,
            major_tick_line_width=big_width,
            minor_tick_line_width=big_width,

            axis_label_text_font_size='15pt',
            major_label_text_font_size='10pt'
        )
    )

    p2.set_select(
        selector=Title,
        updates=dict(text_font_size='20pt')
    )

    p2.set_select(
        selector=Circle,
        updates=dict(size=15)
    )

    r = row([p, p2])

    r.set_select(
        selector=Figure,
        updates=dict(
            plot_width=500,
        )
    )

    # r.set_select(
    #     selector=Plot,
    #     updates=dict(
    #         plot_width=500,
    #     )
    # )

    show(r)

And I am getting this error

    Traceback (most recent call last):
    File "set_select_plot.py", line 81, in <module>
        plot_width=500,
    File "env\lib\site-packages\bokeh-1.3.4-py3.7.egg\bokeh\model.py", line 628, in set_select
        for obj in self.select(selector):
    File "env\lib\site-packages\bokeh-1.3.4-py3.7.egg\bokeh\core\query.py", line 87, in <genexpr>
        return (obj for obj in objs if match(obj, selector, context))
    File "env\lib\site-packages\bokeh-1.3.4-py3.7.egg\bokeh\core\query.py", line 159, in match
        for key, val in selector.items():
    AttributeError: type object 'Figure' has no attribute 'items'

Also, If I use select in a plot like this works well

    p2.select(
        name='p2_circle'
    )

And if I use the method with a layout an error is thrown `TypeError: select() got an unexpected keyword argument 'name'`:

    r.select(
        name='p2_circle'
    )


Discussion:
@chesucr I'm a little unclear. This is uncommented:
```
r.set_select(
    selector=Figure,
    updates=dict(
        plot_width=500,
    )
)
```
Are you saying that works, or not? 
@chesucr Well, it's easy enough to try and see that it doesn't. But this does work:

```
r.set_select(
     selector=dict(type=Figure),
     updates=dict(
         plot_width=500,
     )
)
```
The actual real format of selectors is dictionaries like those above. There are some convenience APIs that try to convert e.g. type values in to `dict(type=...)` automatically, and it seems that's not what is happening here for some reason. Do you want to try and take a look at this? The work is pure Python side (no JS), and I am happy to point you at the relevant modules. Given that this is a very obscure function, and there is a workaround (to use the actual real selector format as above) I doubt this will rise on my priority list any time soon. 
@bryevdv I'd like to take an attempt at this. I have built bokeh from source and can start right away.

What should I look into?

Hi @kumaran-14 The `set_select` function is defined on `Model` here:

https://github.com/bokeh/bokeh/blob/master/bokeh/model.py

and on `Document` here:

https://github.com/bokeh/bokeh/blob/master/bokeh/document/document.py

And there are some minimal test (for Document only) here:

https://github.com/bokeh/bokeh/blob/master/tests/unit/bokeh/document/test_document.py#L301


As I recall the issue was that it was desirable for 
```
r.set_select(selector=Figure, ... )
```
to work as a convenience shorthand for the more verbose:
```
r.set_select(selector={"type": Figure}, ... )
```
I think you can make that happen by simply checking to see if the value of selector is `isinstance` of a Bokeh `Model` inside the function, and if it is, replacing the value with the dict version. It would be good to add some minimal tests for the `Model.set_select` as well as making sure both groups of test cover the case in this issue where the `Selector` passed in is a model. 
I guess I should add, the above is the "quick fix" if you want to look in to things a little more deeply, you could see if it's possible to make updates in only one place, in the `query` module:

https://github.com/bokeh/bokeh/blob/master/bokeh/core/query.py

Both `Model.set_select` and `Document.set_select` ultimately just call `query.find` so if that could be updated then things would be fixed everywhere I think. 

EDIT: tho actually thinking more, I think the "quick fix" might be the best, correct fix. That keeps the `query` module simple and generic and does not require it to know anything about Bokeh models, which I think is preferable. TLDR; ignore my second comment here. 
@bryevdv I was trying to fix it, but faced some problems.
First I executed the above file, reproduced the error and got the output when I used dict instead. Then I trying to debug and play around for some time. It was all fine and suddenly now I could no longer get the output. The html file shows this error.

```
Uncaught TypeError: Cannot read property 'connect' of undefined
    at p.connect (bokeh-2.0.0dev5.min.js:162)
    at p.connect_signals (bokeh-2.0.0dev5.min.js:229)
    at p.finalize (bokeh-2.0.0dev5.min.js:161)
    at bokeh-2.0.0dev5.min.js:148
    at n (bokeh-2.0.0dev5.min.js:148)
    at s (bokeh-2.0.0dev5.min.js:148)
    at Function._initialize_references_json (bokeh-2.0.0dev5.min.js:148)
    at Function.from_json (bokeh-2.0.0dev5.min.js:148)
    at g (bokeh-2.0.0dev5.min.js:146)
    at bokeh-2.0.0dev5.min.js:146
```

The examples given in the developer documentation run correctly though.
Could you help me out?
@kumaran-14 Sure but I really would need some idea of the changes that you made

Also if you are running from master you probably need to build BokehJS locally and make sure to specify `BOKEH_RESOURCES=inline`. I can see you have loaded 2.0.0dev5 from CDN but we have not had a dev build in a while and there may be changes on master since then that are incompatible. 
Thats the issue. I tried it after ```git reset --hard```. It still persists.
See above edit about using locally built BokehJS
@bryevdv  thanks for the quick response. It works now. 
Now, I'm trying ```isinstance(selector, Model)```, which seems to evaluate to false. Is this what you suggested? Or Am I wrong?
Thanks again.
Sorry, mis-typed earlier. These are types so you want `issubclass`:
```
In [1]: from bokeh.model import Model

In [2]: from bokeh.models import Plot

In [3]: issubclass(Plot, Model)
Out[3]: True
```
Thanks. 
In `/bokeh/model.py`, I added few these few lines

```
	# added these
	if (isinstance(selector, dict)):
        selector = selector["type"]
    if (issubclass(selector, Model)):
        selector = dict(type=selector)
	
	# existing code
    for obj in self.select(selector):
        for key, val in updates.items():
            setattr(obj, key, val)
```

Now, In above example, all these work correctly.

```
	.
	.
	.

	r.set_select(
	     selector=dict(type=Figure),
	     updates=dict(
	         plot_width=500,
	     )
	)
	
	r.set_select(
	    selector=Figure,
	    updates=dict(
	        plot_width=500,
	    )
	)
	
	r.set_select(
	    selector=Plot,
	    updates=dict(
	        plot_width=500,
	    )
	)

	show(r)

```

Is this close to what you had in mind? Do you think hardcoding the keyname 'type' is right? 
@kumaran-14 I don't think the first two lines:
```
if (isinstance(selector, dict)):
    selector = selector["type"]
```
are necessary. `dict` is the actual real format for selectors, there is no need to check for them, they can/should just be passed through as-is. The only task is to check if a passed-in selector value is a `Model` instead of a `dict` and then in that case, construct the `dict` with key "type" as a convenience. 

I would suggest opening a PR with the changes to `model.py` and `document.py` as a next step.  It's easy for comment streams on issues to get lost in a mountain of emails, but there are relatively few PRs so it is much easier to stay on top of. 
@bryevdv Sure.
My pre-commit hook `py.test -m codebase` fails due to wrong import order in some files.
I run this on unchanged working tree of bokeh upstream.
How do I fix this?
@kumaran-14 you need to  make sure you have done `pip install channels` first. There is not a conda package for it yet, and `isort` gets confused about some third part vs package imports if it is not installed. 

Resolving patch:
diff --git a/bokeh/document/document.py b/bokeh/document/document.py
--- a/bokeh/document/document.py
+++ b/bokeh/document/document.py
@@ -35,6 +35,7 @@
 import sys
 from collections import defaultdict
 from functools import wraps
+from inspect import isclass
 from json import loads
 from typing import Any, Callable, Dict, List
 
@@ -48,6 +49,7 @@
 from ..core.templates import FILE
 from ..core.validation import check_integrity
 from ..events import Event
+from ..model import Model
 from ..themes import Theme, built_in_themes
 from ..themes import default as default_theme
 from ..util.callback_manager import _check_callback
@@ -861,6 +863,8 @@ def set_select(self, selector, updates):
             None
 
         '''
+        if isclass(selector) and issubclass(selector, Model):
+            selector = dict(type=selector)
         for obj in self.select(selector):
             for key, val in updates.items():
                 setattr(obj, key, val)
diff --git a/bokeh/model.py b/bokeh/model.py
--- a/bokeh/model.py
+++ b/bokeh/model.py
@@ -19,6 +19,7 @@
 #-----------------------------------------------------------------------------
 
 # Standard library imports
+from inspect import isclass
 from json import loads
 from operator import itemgetter
 
@@ -557,6 +558,8 @@ def set_select(self, selector, updates):
             None
 
         '''
+        if isclass(selector) and issubclass(selector, Model):
+            selector = dict(type=selector)
         for obj in self.select(selector):
             for key, val in updates.items():
                 setattr(obj, key, val)


Test patch:
diff --git a/tests/unit/bokeh/document/test_document.py b/tests/unit/bokeh/document/test_document.py
--- a/tests/unit/bokeh/document/test_document.py
+++ b/tests/unit/bokeh/document/test_document.py
@@ -308,11 +308,13 @@ def test_select(self) -> None:
         root2 = SomeModelInTestDocument(foo=44, name='c')
         root3 = SomeModelInTestDocument(foo=44, name='d')
         child3 = SomeModelInTestDocument(foo=45, name='c')
+        root4 = AnotherModelInTestDocument(bar=20, name='A')
         root1.child = child1
         root3.child = child3
         d.add_root(root1)
         d.add_root(root2)
         d.add_root(root3)
+        d.add_root(root4)
 
         # select()
         assert set([root1]) == set(d.select(dict(foo=42)))
@@ -348,6 +350,15 @@ def test_select(self) -> None:
         assert set([child3, root3]) == set(d.select(dict(foo=57)))
         assert set([child3, root3]) == set(root3.select(dict(foo=57)))
 
+        # set_select() on class
+        d.set_select(SomeModelInTestDocument, dict(name='new_name'))
+        assert len(d.select(dict(name='new_name'))) == 5
+
+        # set_select() on different class
+        assert len(d.select(dict(name='A'))) == 1
+        d.set_select(AnotherModelInTestDocument, dict(name='B'))
+        assert set([root4]) == set(d.select(dict(name='B')))
+
     def test_is_single_string_selector(self) -> None:
         d = document.Document()
         # this is an implementation detail but just ensuring it works
diff --git a/tests/unit/bokeh/test_model.py b/tests/unit/bokeh/test_model.py
--- a/tests/unit/bokeh/test_model.py
+++ b/tests/unit/bokeh/test_model.py
@@ -20,6 +20,8 @@
 from bokeh.models import CustomJS
 from bokeh.plotting import *  # NOQA
 
+from bokeh.document import document # isort:skip
+
 # Module under test
 from bokeh.model import Model # isort:skip
 
@@ -134,6 +136,55 @@ def test_all_builtin_models_default_constructible() -> None:
             bad.append(name)
         assert bad == []
 
+def test_select() -> None:
+    # we aren't trying to replace test_query here, only test
+    # our wrappers around it, so no need to try every kind of
+    # query
+    d = document.Document()
+    root1 = SomeModel(a=42, name='a')
+    root2 = SomeModel(a=43, name='c')
+    root3 = SomeModel(a=44, name='d')
+    root4 = SomeModel(a=45, name='d')
+    d.add_root(root1)
+    d.add_root(root2)
+    d.add_root(root3)
+    d.add_root(root4)
+
+    # select()
+    assert set([root1]) == set(root1.select(dict(a=42)))
+    assert set([root1]) == set(root1.select(dict(name='a')))
+    assert set([root2])  == set(root2.select(dict(name='c')))
+    assert set()  == set(root1.select(dict(name='nope')))
+
+    # select() on object
+    assert set() == set(root3.select(dict(name='a')))
+    assert set([root3]) == set(root3.select(dict(a=44)))
+
+    # select_one()
+    assert root3 == root3.select_one(dict(name='d'))
+    assert None == root1.select_one(dict(name='nope'))
+
+    with pytest.raises(ValueError) as e:
+        d.select_one(dict(name='d'))
+    assert 'Found more than one' in repr(e)
+
+    # select_one() on object
+    assert None == root3.select_one(dict(name='a'))
+    assert None == root3.select_one(dict(name='c'))
+
+    # set_select()
+    root1.set_select(dict(a=42), dict(name='c', a=44))
+    assert set([root1]) == set(root1.select(dict(name='c')))
+    assert set([root1])  == set(root1.select(dict(a=44)))
+
+    # set_select() on object
+    root3.set_select(dict(name='d'), dict(a=57))
+    assert set([root3]) == set(root3.select(dict(a=57)))
+
+    # set_select() on class
+    root2.set_select(SomeModel, dict(name='new_name'))
+    assert set([root2]) == set(root2.select(dict(name="new_name")))
+
 #-----------------------------------------------------------------------------
 # Dev API
 #-----------------------------------------------------------------------------