Subclassing bokeh model

[Bokeh 2.3.1]
Hey there,

I’m trying to subclass bokeh.models.widgets.Slider to make it have a throttled behaviour (delay-based not mouse-up based)

For this I grabbed the code from here and modified it a bit: Server side throttled slider - #3 by Matt_Agee .

Modified Code
from time import perf_counter
from functools import partial

from bokeh.models.widgets import Slider
from bokeh.io import curdoc

class ThrottledSlider(Slider):
    """
    Extends the bokeh RangeSlider to provide throttling of callbacks provided to on_update
    """
    __view_model__  = "Slider"
    __subtype__ = ""
    def __init__(self, base_delay, doc=curdoc(), *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._cb_set = False
        self._base_delay = base_delay
        self._delay = base_delay
        self._doc = doc

    def on_change(self, attrname, callback):
        """
        Register a server side callback to be executed when slider attributes change
        """
        print("a")
        update_function = self._callback_wrapper(callback)
        super().on_change(attrname, partial(self._callback_setter, update_function))

    def _callback_setter(self, update_function, attrname, old, new):
        if not self._cb_set:
            self._doc.add_timeout_callback(partial(update_function, attrname, old), self._delay)
            self._cb_set = True

    def _callback_wrapper(self, callback):
        """
        Wraps callback to use self.value instead of new.
        This ensures the callback has the most recent slider value at execution.

        Also modifies the delay between callback execution to account for time taken
        performing the callback's work.
        """
        def throttled_cb(*args):
            start_time = perf_counter()
            attr, old = args
            callback(attr, old, self.value)

            # Modify delay to account for time taken executing callback
            self._delay = max(self._base_delay - 1000 * (perf_counter() - start_time), 50)
            self._cb_set = False
        return throttled_cb

However, the proposed workaround (setting __view_model__ and __subtype__) does not seem to work anymore. (bokeh complains that the model was not registered)

So, as suggested in GoogleGroups I wrote some TypeScript:

TSCode
import * as p from "core/properties"
import {Slider, SliderView} from "models/widgets/slider"

export class ThrottledSliderView extends SliderView {
  model: ThrottledSlider
}
  
export namespace ThrottledSlider {
  export type Attrs = p.AttrsOf<Props>
  export type Props = Slider.Props
}

export interface ThrottledSlider extends ThrottledSlider.Attrs {}

export class ThrottledSlider extends Slider {
  properties: ThrottledSlider.Props
  __view_type__: ThrottledSliderView

  constructor(attrs?: Partial<ThrottledSlider.Attrs>) {
    super(attrs)
  }

  static init_ThrottledSlider() {
    this.prototype.default_view = ThrottledSliderView
  }
}

The slider shows up, but on_change does not fire. What is the right way to do it in 2021? How do I subclass Slider trivially (and properly), so that the on_change event fires properly?

As usual, I’m a dummy. Calling curdoc() as a default parameter was the culprit, as it already gets resolved somewhere in the abyss of the bokeh inner-workings *sigh*. Everything works fine with my TypeScript. Now I can work on cutting out unnecessary parts of the TypeScript-portion.

Edit:
Seems like this is enough for the TypeScript code; trivial, indeed:

import {Slider} from "models/widgets/slider"

export class ThrottledSlider extends Slider {}

After some digging I found out that by setting

__view_model__="Slider"
and
__implementation__=""
I can get around doing the TypeScript stuff.

This is due to how __init_subclass__ in model.py is coded.

__init_subclass__
    def __init_subclass__(cls):
    super().__init_subclass__()

    # use an explicitly provided view model name if there is one
    if "__view_model__" not in cls.__dict__:
        cls.__view_model__ = cls.__name__
    if "__view_module__" not in cls.__dict__:
        cls.__view_module__ = cls.__module__

    module = cls.__view_module__
    model = cls.__dict__.get("__subtype__", cls.__view_model__)
    impl = cls.__dict__.get("__implementation__", None)

    head = module.split(".")[0]
    if head == "bokeh" or head == "__main__" or impl is not None:
        qualified = model  # <--- this is where we go now
    else:
        qualified = module + "." + model  # this is where we go without the "hack"
    cls.__qualified_model__ = qualified
    ...

What is the recommended way? Does this more hacky method skip widget compilation? Should I stick to the version in the post above? @Bryan (I hope it’s okay to ping you)

I’m not going to be the best person to comment on the current state of things. cc @mateusz who is in a better position to offer guidance.

Alright, thanks for the heads up.

@mateusz do you maybe have any recommendations?

You should remove:

__view_model__  = "Slider"
__subtype__ = ""

(this is intended for no-implementation models) and add

static __module__ = "fully.qualified.module.name.matching.python.module"

to your JS/TS model definition. See

as an example. Mismatch between Python and JS/TS module name is the reason for “not found” error.