holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.62k stars 505 forks source link

Combine Existing Components example, EditableRange(Viewer), setting .value not working as expected #6928

Open fohrloop opened 2 months ago

fohrloop commented 2 months ago

ALL software version info

Description of expected behavior and the observed behavior

Following the example in Combine Existing Components. The EditableRange.value does not work as expected.

Complete, minimal, self-contained example code that reproduces the issue

This one is copied from the docs (Combine Existing Components).

import panel as pn
import param
from panel.viewable import Viewer

class EditableRange(Viewer):

    value = param.Range(doc="A numeric range.")

    width = param.Integer(default=300)

    def __init__(self, **params):
        self._start_input = pn.widgets.FloatInput()
        self._end_input = pn.widgets.FloatInput(align="end")
        super().__init__(**params)
        self._layout = pn.Row(self._start_input, self._end_input)
        self._sync_widgets()

    def __panel__(self):
        return self._layout

    @param.depends("value", "width", watch=True)
    def _sync_widgets(self):
        self._start_input.name = self.name
        self._start_input.value = self.value[0]
        self._end_input.value = self.value[1]
        self._start_input.width = self.width // 2
        self._end_input.width = self.width // 2

    @param.depends("_start_input.value", "_end_input.value", watch=True)
    def _sync_params(self):
        self.value = (self._start_input.value, self._end_input.value)
class TestEditableRange:
    def test_value_change(self):
        er = EditableRange(name="foo", value=(25.0, 75.0))
        assert er.value == (25.0, 75.0)

        er.value = (20.0, 80.0)
        assert er.value == (20.0, 80.0)

Stack traceback and/or browser JavaScript console output

This is what you get from the test:

self = <mypkg.TestEditableRange object at 0x7933b1d8aa80>

    def test_value_change(self):
        er = EditableRange(name="foo", value=(25.0, 75.0))
>       assert er.value == (25.0, 75.0)
E       assert (25.0, 0) == (25.0, 75.0)
E         
E         At index 1 diff: 0 != 75.0
E         Use -v to get more diff

tests/test_example.py:130: AssertionError

Screenshots or screencasts of the bug in action

hoxbro commented 2 months ago

I think what is happening is the change to _start_input.value in _sync_widgets is updating self.value in _sync_params before _end_input has been updated.

    @param.depends("value", "width", watch=True)
    def _sync_widgets(self):
        self._start_input.name = self.name
        with (
            param.discard_events(self._start_input),
            param.discard_events(self._end_input)
        ):
            self._start_input.value = self.value[0]
            self._end_input.value = self.value[1]
        self._sync_params()
        self._start_input.width = self.width // 2
        self._end_input.width = self.width // 2