Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it easy to use dataclass like models using familiar apis #6912

Open
wants to merge 65 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
cd58f50
improve ipywidgets reference guide
MarcSkovMadsen Jun 5, 2024
e75776a
improve language
MarcSkovMadsen Jun 5, 2024
19c9741
clean notebook
MarcSkovMadsen Jun 5, 2024
428ec92
fix reference links
MarcSkovMadsen Jun 5, 2024
86b5002
explain sizing better
MarcSkovMadsen Jun 5, 2024
71287e4
pre-commit
MarcSkovMadsen Jun 5, 2024
8b41a07
design spec including tests
MarcSkovMadsen Jun 11, 2024
f0a766d
support layout kwargs
MarcSkovMadsen Jun 11, 2024
1da8658
use panel
MarcSkovMadsen Jun 11, 2024
17a39e3
update design spec
MarcSkovMadsen Jun 11, 2024
4907090
implement rx
MarcSkovMadsen Jun 11, 2024
42b2d82
implement first version
MarcSkovMadsen Jun 11, 2024
00b5731
add to_viewer back
MarcSkovMadsen Jun 11, 2024
a8021a5
carve out sync_parameterized
MarcSkovMadsen Jun 11, 2024
1c18076
major refactor of ipywidget
MarcSkovMadsen Jun 12, 2024
09f6a1c
fix remaining todos
MarcSkovMadsen Jun 12, 2024
12583df
skip test if not ipywidgets available
MarcSkovMadsen Jun 13, 2024
883eb7b
carve out and add back in other PR
MarcSkovMadsen Jun 13, 2024
d80a506
Merge branch 'docs/ipywidgets-reference-update' into ipywidget_utility
MarcSkovMadsen Jun 13, 2024
56ed52e
update names and improve docstrings
MarcSkovMadsen Jun 13, 2024
c21c865
use model and names terminology
MarcSkovMadsen Jun 15, 2024
025659b
support wrapping model Classes
MarcSkovMadsen Jun 15, 2024
e649aeb
support WidgetViewer from class
MarcSkovMadsen Jun 15, 2024
83a5f93
add missing test
MarcSkovMadsen Jun 15, 2024
0a83798
make _names an attribute instead of parameter
MarcSkovMadsen Jun 15, 2024
771f986
simplify to model class
MarcSkovMadsen Jun 15, 2024
2f0f175
expose model
MarcSkovMadsen Jun 15, 2024
8471e49
clean up tests
MarcSkovMadsen Jun 15, 2024
36217c9
document. had to move to wrappers module
MarcSkovMadsen Jun 15, 2024
aa0eaa3
rename to observers
MarcSkovMadsen Jun 15, 2024
b1da06b
rename for more generality and alignment with observer pattern
MarcSkovMadsen Jun 15, 2024
7661402
first version of how-to guide
MarcSkovMadsen Jun 15, 2024
b2ad70b
docs review
MarcSkovMadsen Jun 15, 2024
88344a2
add comment
MarcSkovMadsen Jun 15, 2024
d1b5d9e
remove example files
MarcSkovMadsen Jun 15, 2024
56d0740
Merge branch 'main' of https://github.com/holoviz/panel into ipywidge…
MarcSkovMadsen Jun 15, 2024
bcd21a4
review feedback
MarcSkovMadsen Jun 15, 2024
3669877
docs review feedback
MarcSkovMadsen Jun 15, 2024
4a20c94
fix links
MarcSkovMadsen Jun 15, 2024
fdd25bf
refactor to dataclass namespace
MarcSkovMadsen Jun 16, 2024
6e1215d
rename _names to names and _model_names
MarcSkovMadsen Jun 16, 2024
b178ebf
refactor to support Pydantic too
MarcSkovMadsen Jun 16, 2024
289a125
update table
MarcSkovMadsen Jun 16, 2024
6f9c4f5
add ideas for observing pydantic
MarcSkovMadsen Jun 16, 2024
2ae5d19
Various cleanup
philippjfr Jun 17, 2024
a543719
Add pydantic to pixi deps
philippjfr Jun 17, 2024
287d240
Fix indexes
philippjfr Jun 17, 2024
08819f7
Fix index
philippjfr Jun 17, 2024
8baab7c
update names for consistency
MarcSkovMadsen Jun 17, 2024
60f7964
Align docstring
philippjfr Jun 17, 2024
a3c7b4e
Optimize and fix parameter syncing
philippjfr Jun 17, 2024
0fc8092
Rename create_ functions to to_
philippjfr Jun 18, 2024
6d8c2a3
Reorganize dataclass module
philippjfr Jun 18, 2024
11685c8
Convert parameter types
philippjfr Jun 18, 2024
fb2e77d
Merge branch 'main' into ipywidget_utility
philippjfr Jun 21, 2024
1aff765
Serialize datetime objects
philippjfr Jun 22, 2024
3bd2fad
Allow dev version in base_version
philippjfr Jun 22, 2024
bf17799
Merge remote-tracking branch 'origin/main' into ipywidget_utility
MarcSkovMadsen Jul 13, 2024
3cac03e
review feedback
MarcSkovMadsen Jul 14, 2024
ef1521f
fix
MarcSkovMadsen Jul 14, 2024
0a9902a
pydantic parameters + default value
MarcSkovMadsen Jul 14, 2024
11a896d
add missing pydantic parameters
MarcSkovMadsen Jul 15, 2024
f7ee165
fix tuple exception
MarcSkovMadsen Jul 15, 2024
fc76ad4
add ModelForm
MarcSkovMadsen Jul 15, 2024
d364cfe
refactor
MarcSkovMadsen Jul 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions examples/reference/panes/examples/ipywidgets_rx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Original Source: https://github.com/opengeos/solara-geospatial/blob/main/pages/01_leafmap.py
MarcSkovMadsen marked this conversation as resolved.
Show resolved Hide resolved
import leafmap
MarcSkovMadsen marked this conversation as resolved.
Show resolved Hide resolved

from leafmap.toolbar import change_basemap

import panel as pn

from panel.ipywidget import to_rx

pn.extension("ipywidgets")

class Map(leafmap.Map):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Add what you want below
self.add_basemap("OpenTopoMap")
change_basemap(self)


widget = Map( # type: ignore
zoom=2,
center=(20,0),
scroll_wheel_zoom=True,
toolbar_ctrl=False,
data_ctrl=False,
)

zoom, center = to_rx(widget, "zoom", "center")

layout = pn.Column(
widget,
# I would like to add a zoom_input widget created_from and synced to the zoom rx variable.
# But its currently not possible in one line of code. See https://github.com/holoviz/panel/issues/6911
zoom, center,
pn.Row(
pn.pane.Markdown(pn.rx("Zoom: {zoom}").format(zoom=zoom)),
pn.pane.Markdown(pn.rx("Center: {center}").format(center=center)),
),
)

pn.template.FastListTemplate(
site="🌎 Panel Geospatial",
site_url="./",
title="Leafmap",
main=[layout],
main_layout=None,
accent="teal",
).servable()

# Todo: Report error in console when tool clicked to ipywidgets_bokeh and leafmap
# Todo: Report missing fonts in terminal when app is server to ipywidgets_bokeh and leafmap
56 changes: 56 additions & 0 deletions examples/reference/panes/examples/ipywidgets_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Original Source: https://github.com/opengeos/solara-geospatial/blob/main/pages/01_leafmap.py
MarcSkovMadsen marked this conversation as resolved.
Show resolved Hide resolved
import leafmap
import param

from leafmap.toolbar import change_basemap

import panel as pn

from panel.ipywidget import to_viewer

pn.extension("ipywidgets")

class Map(leafmap.Map):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Add what you want below
self.add_basemap("OpenTopoMap")
change_basemap(self)

widget = Map( # type: ignore
zoom=2,
center=(20,0),
scroll_wheel_zoom=True,
toolbar_ctrl=False,
data_ctrl=False,
)

class MapViewer(param.Parameterized):
zoom = param.Number(default=2, bounds=(0,24), step=1)
center = param.List([20,0])

def __init__(self, **params):
super().__init__(**params)
print("init MapViewer")

viewer = to_viewer(widget, sizing_mode="stretch_width", height=700, styles={"border": "1px solid black"})


layout = pn.Column(
viewer, # Todo: get viewer height, width and sizing_mode working
pn.Row(
viewer.param.zoom,
viewer.param.center,
pn.pane.Markdown(pn.rx("Zoom: {zoom}").format(zoom=viewer.param.zoom)),
pn.pane.Markdown(pn.rx("Center: {center}").format(center=viewer.param.center)),
),
)

pn.template.FastListTemplate(
site="🌎 Panel Geospatial",
site_url="./",
title="Leafmap",
main=[layout],
main_layout=None,
accent="teal",
).servable()
217 changes: 217 additions & 0 deletions panel/ipywidget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"""Functionality to enable easily interacting with ipywidgets via familiar APIs like pn.bind,
@pn.depends and pn.rx"""

from inspect import isclass
from typing import TYPE_CHECKING, Any, Iterable

import param

from param import Parameterized
from param.reactive import bind

from .pane import IPyWidget
from .viewable import Layoutable, Viewer

if TYPE_CHECKING:
try:
from traitlets import HasTraits
except ModuleNotFoundError:
HasTraits = Any

try:
from ipywidgets import Widget
except ModuleNotFoundError:
Widget = Any
else:
HasTraits = Any
Widget = Any


def _is_custom_trait(name):
if name.startswith("_"):
return False
if name in {"comm", "tabbable", "keys", "log", "layout"}:
return False
return True


def _get_public_and_relevant_trait_names(widget):
return tuple(name for name in widget.traits() if _is_custom_trait(name))


def sync_parameterized(widget: HasTraits, parameterized: Parameterized, *parameters):
"""Syncs the parameters of the widget and the parameters of the parameterized

Please note we don't sync to a readonly widget parameter. We do sync to a constant
parameterized parameter though.

Args:
widget: The widget to keep synced
parameterized: The Parameterized to keep synced
parameters: The names of the parameters to keep synced. If none are specified all public and
relevant parameters of the widget will be synced.
"""
if not parameters:
parameters = _get_public_and_relevant_trait_names(widget)

with param.edit_constant(parameterized):
for parameter in parameters:
setattr(parameterized, parameter, getattr(widget, parameter))

for parameter in parameters:
# Observe widget parameter
def _handle_widget_change(_, widget=widget, parameter=parameter):
with param.edit_constant(parameterized):
setattr(parameterized, parameter, getattr(widget, parameter))
widget.observe(_handle_widget_change, names=parameter)

# Bind to parameterized parameter
read_only = set()

def _handle_observer_change(_, widget=widget, parameter=parameter, read_only=read_only):
if parameter not in read_only:
try:
setattr(widget, parameter, getattr(parameterized, parameter))
except Exception:
read_only.add(parameter)

bind(_handle_observer_change, parameterized.param[parameter], watch=True)

class HasTraitsParameterized(Parameterized):
"""An abstract base class for creating a Parameterized that wraps a HasTraits"""

_widget = param.Parameter(allow_None=False)
_parameters = param.Parameter(allow_None=False)

def __init__(self, **params):
super().__init__(**params)

sync_parameterized(self._widget, self, *self._parameters)

_ipywidget_classes = {}


def _to_tuple(
bases: None | Parameterized | Iterable[Parameterized],
) -> tuple[Parameterized]:
if not bases:
bases = ()
if isclass(bases) and issubclass(bases, Parameterized):
bases = (bases,)
return tuple(item for item in bases)


def to_parameterized(
MarcSkovMadsen marked this conversation as resolved.
Show resolved Hide resolved
widget: HasTraits,
*parameters,
bases: Iterable[Parameterized] | Parameterized | None = None,
**kwargs,
) -> Parameterized:
"""Returns a Parameterized object with parameters synced to the ipywidget widget parameters

Args:
widget: The ipywidget to create the Viewer from.
parameters: The parameters to add to the Parameterized and to sync.
If no parameters are specified all public parameters on the widget will be added
and synced.
"""
if not parameters:
parameters = _get_public_and_relevant_trait_names(widget)
bases = _to_tuple(bases) + (HasTraitsParameterized,)
name = type(widget).__name__
key = (name, parameters, bases)
if name in _ipywidget_classes:
parameterized = _ipywidget_classes[key]
else:
existing_params = ()
for base in bases:
existing_params += tuple(base.param)
params = {
name: param.Parameterized()
for name in parameters
if name not in existing_params
}

parameterized = param.parameterized_class(name, params=params, bases=bases)
# Todo: Figure out why not all parameters are added
for parameter in params:
if parameter not in parameterized.param:
parameterized.param.add_parameter(parameter, param.Parameter())

_ipywidget_classes[key] = parameterized
instance = parameterized(_widget=widget, _parameters=parameters, **kwargs)

return instance


class IpyWidgetViewer(Layoutable, Viewer):
"""An abstract base class for creating a Layoutable Viewer that wraps an ipywidget"""

_widget = param.Parameter(allow_None=False)

def __init__(self, **params):
super().__init__(**params)

widget = self._widget
widget.height = "100%"
widget.width = "100%"
layout_params = {name: self.param[name] for name in Layoutable.param}

self._layout = IPyWidget(widget, **layout_params)

def __panel__(self):
return self._layout


def to_viewer(
widget: Widget,
*parameters,
bases: Parameterized | None = None,
**kwargs,
) -> Viewer:
"""Returns a Parameterized object with parameters synced to the ipywidget widget parameters

Args:
widget: The ipywidget to create the Viewer from.
parameters: The parameters to add to the Parameterized and to sync.
If no parameters are specified all public parameters on the widget will be added
and synced.
"""
bases = _to_tuple(bases) + (IpyWidgetViewer,)

return to_parameterized(widget, *parameters, bases=bases, **kwargs)


def sync_rx(element: HasTraits, name: str, target: param.rx):
"""Syncs the element name attribute and the target.rx.value"""
target.rx.value = getattr(element, name)

def set_value(event, target=target):
target.rx.value = event["new"]

element.observe(set_value, names=name)

def set_name(value, element=element, name=name):
setattr(element, name, value)

target.rx.watch(set_name)


def to_rx(widget: HasTraits, *parameters) -> param.rx | tuple[param.rx]:
"""Returns a tuple of `rx` parameters. Each one synced to a parameter of the widget.

Args:
widget: The widget to create the `rx` parameters from.
parameters: The parameter or parameters to create `rx` parameters from and to sync.
If a single parameter is specified a single reactive parameter is returned.
If no parameters are specified all public and relevant parameters of the widget will be
used.
"""
rx_values = []
for name in parameters:
rx = param.rx()
sync_rx(widget, name, rx)
rx_values.append(rx)
if len(rx_values) == 1:
return rx_values[0]
return tuple(rx_values)
Loading
Loading