Callback arg won't keep track of changes to `HasProps` subclass property?

Hiya, hopefully I’ll be able to describe this adequately!

I’m working on a way to use Bokeh to make an interactive data analysis tool: GitHub - 0Hughman0/htools

I’ve worked out how to make custom Python ‘dataclasses’ that I can use in Python and then use in JS callbacks e.g.:

from bokeh.models.widgets.buttons import Button
from bokeh.models.callbacks import CustomJS
from bokeh.core.properties import String
from bokeh.core.has_props import HasProps
from bokeh.io import output_notebook, show

output_notebook()

class DataClass(HasProps):
    text = String(default='Default')

d = DataClass()
d.text = "Hello"
b = Button(label='Do')
b.js_on_click(CustomJS(code="window.alert(d.text);", args={'d': d}))
show(b)

So clicking the button, ‘Hello’ will pop up as expected.

However subsequently running:

d.text = 'Goodbye'

and then clicking the button, rather than ‘Goodbye’ popping up, ‘Hello’ still appears.

This is problem for me because I want the callback to keep track of the current state of my Python objects, and look this up every time I click a button. Thing is as far as I can tell, passing Bokeh models as callbacks the current state of the model is used rather than this seemingly cached value.

Perhaps I’m barking up the wrong tree, and help or advice would be appreciated! (I’d also like to know how you guys test code that is both written in JS and for Jupyter notebooks!)

Using Bokeh ‘1.2.0’ and Jupyter ‘4.4.0’.

Python and JS “sides” are not automatically synced inside notebooks except in the two cases:

  • use of push_notebook (explicit one-way updates, Python → JS)
  • an embedded Bokeh server app, (bidirectional automatic updates)

Neither of which happens in the above code [1]. You can find examples of push_notebook here:

https://github.com/bokeh/bokeh/tree/master/examples/howto/notebook_comms

And an example of how to embed a Bokeh server application in notebooks here:

https://github.com/bokeh/bokeh/blob/master/examples/howto/server_embed/notebook_embed.ipynb

I’d also like to know how you guys test code that is both written in JS and for Jupyter notebooks!

At present: manually. We had infrastructure set up to do some level of automatic notebook testing in the CI builds in the past, but it was broken awhile ago and there has not been time or resources to bring it back up to date. Even the past tests were just advisory (non-failing) image-diff reports, and checks that there were no unexpected JS console errors. We definitely never got to the level of e.g. Selenium tests for notebook interactions.


  1. This reason for this is that the possibility of arbitrary, out of order cell execution (the notebook’s worst mis-feature IMO) means that it quickly becomes impossible to reason about what an implicit, shared synchronized state between two large, cross-runtime systems even should be, nevermind what it actually is. Bokeh demands some level of explicitness to maintain sanity. ↩︎

Thank you very much for the response.

I figured something like push_notebook may be required here. However when I try simply changing the second block to:

d.text = 'Goodbye'
push_notebook()

The value is not updated?

However, I have since realised that the implementation I want to include this sort of behaviour in is an embedded server app anyway. My understanding is that in this case I can only make changes to model properties after rendering as part of function callbacks?

Thanks for the info regarding testing. I feel slightly less bad for just doing it by eye in that case.

Cheers for the help.

To investigate or speculate, a complete minimal reproducer is really needed.

My understanding is that in this case I can only make changes to model properties after rendering as part of function callbacks?

Yes changes to Bokeh model properties have to happen in Bokeh callback, e.g. *on_change`, periodic or timeout callbacks.

See below:

from bokeh.models.widgets.buttons import Button
from bokeh.models.callbacks import CustomJS
from bokeh.core.properties import String
from bokeh.core.has_props import HasProps
from bokeh.io import output_notebook, show, push_notebook

output_notebook()

class DataClass(HasProps):
    text = String(default='Default')

d = DataClass()
d.text = "Hello"
b = Button(label='Do')
b.js_on_click(CustomJS(code="window.alert(d.text);", args={'d': d}))
show(b, notebook_handle=True)
d.text = 'Goodbye'
push_notebook()
# Click the button -> Still says "Hello"

I wonder if I need to do something weird with the push_notebook?

(as before using Bokeh ‘1.2.0’ and Jupyter ‘4.4.0’.)

@0Hughman0 It was not evident that you were trying to create your own HasProps subclass. In general that is a very uncommon thing to do, and basically only ever makes sense if you are creating a Custom Extension, and then you would subclass Model, not HasProps. I can’t think of any reason for anyone to subclass HasProps directly.

When I referred to automatic syncing above, I was referring to Bokeh’s built-in models. If you want to create your own custom Model that is automatically synced, that is definitely possibly, but you have to supply the JavaScript implementation for it, just like all the built-in models have. See the link on Custom Extensions above.

Hiya,

I am working to build fairly significant functionality on top of Bokeh, I dunno if I’d describe it as an extension though:

I’m working on a way to use Bokeh to make an interactive data analysis tool: GitHub - 0Hughman0/htools

RE:

I can’t think of any reason for anyone to subclass HasProps directly.

The reason this seemed like this might be the right thing to do is because I assumed Model is for classes which have significant functionality in JS, whereas all I want is an object that holds information about the state of my application, but it does need to be synced. Evidently subclassing HasProps is enough for 1 off transfer of data but not quite what I need.

I did actually try using Model originally but got bogged down in the typescript/ JS as I have little experience with these languages. From the examples I found it wasn’t clear what I should write (although the one you have linked to seems a little more straightforward).

Skimming over the JS implementation you have linked, I would imagine that it would be feasible to create a dataclasses.dataclass like class decorator that takes a Model subclass, looks up it’s core.properties and fills in a template to implement it’s JS.

If you think such a thing would be a valuable addition to the library then I would consider taking the time to implement such a thing. However given how confused you are that this is something I’d like to do, I wonder if there might be a better way!

Understandable if you don’t have time to look, but the use-case is I want to add an if block in:

where for example, start_span will only be moved if RangeAnalyser.start_connected (an attribute of not a model) is true:

Which I think I would achieve by passing RangeAnalyser.start_connected as some sort of parameter to. RangeAnalyserGui.range_slider.js_on_change('value', ...)

Here is a screenshot of RangeAnalyser in action:

Once again, thanks for your help,

Hugh

@0Hughman0 something like that would actually be immensely useful. We have had a long-standing open issue to make “Namespace” models that could and automatically synchronizable place for “scratch variables”. We have imagined it in terms of a kind of dict namespace, and this has not fit in well which has been why it was never finished.

But! Your comments make me reconsider that another approach would be to make dataclass-like Bokeh Models work for users without writing any JS at all. As it happens core contributor @mateusz has also had some ideas along these lines and is currently working on a revamp of the entire compiler subsystem. So I’d say hold off and starting to implement anything just yet. But I have invited him here to join this discussion so that we can make sure to capture and consider your use-case specifically. (We might want to move some discussion to GitHub at some point)

dataclass-like Bokeh Models work for users without writing any JS at all.

This sounds very doable to me, and I think could be a very readable way to implement such a feature, especially as more users get used to using the dataclasses library. With the added bonus that users who are scared of writing type script/ JS (like me!) aren’t forced into writing what is essentially boilerplate code.

1 Like

Just noting that I finally got around to updating the main issue for this and referencing this discussion:

1 Like