-
-
Notifications
You must be signed in to change notification settings - Fork 525
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
base: main
Are you sure you want to change the base?
Conversation
Hi @philippjfr . Would you take a look at the design spec, i.e. the current files? Thanks. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6912 +/- ##
==========================================
+ Coverage 81.71% 81.94% +0.22%
==========================================
Files 326 331 +5
Lines 48082 48861 +779
==========================================
+ Hits 39292 40040 +748
- Misses 8790 8821 +31 ☔ View full report in Codecov by Sentry. |
await sleep(0.250) | ||
return json.loads(model.json()) | ||
exclude = list(layout_params) | ||
def view_model(*args): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why exclude attributes of a Pydantic model?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think there was a good reason.
No worries. I'm happy. |
Not sure I love |
1.
|
Docs build now working. |
FIXED I can see that the ipywidgets import panel as pn
import ipyleaflet as ipyl
pn.extension("ipywidgets")
leaflet_map = ipyl.Map(zoom=4)
viewer = pn.dataclass.ModelViewer(model=leaflet_map, sizing_mode="stretch_both")
pn.Row(pn.Column(viewer.param, scroll=True), viewer, height=400).servable() The problem is that the If I change to use then it can error ValueError: Attribute 'length' of Tuple parameter 'Map.bounds' is not of the correct length (0 instead of 2).
Traceback (most recent call last):
File "/home/jovyan/repos/private/panel/panel/_dataclasses/base.py", line 156, in sync_with_parameterized
setattr(model, field, parameter_value)
File "/home/jovyan/repos/private/panel/.venv/lib/python3.11/site-packages/traitlets/traitlets.py", line 715, in __set__
raise TraitError('The "%s" trait is read-only.' % self.name)
traitlets.traitlets.TraitError: The "bounds" trait is read-only.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/jovyan/repos/private/panel/panel/io/handlers.py", line 389, in run
exec(self._code, module.__dict__)
File "/home/jovyan/repos/private/panel/script.py", line 8, in <module>
viewer = pn.dataclass.ModelViewer(model=leaflet_map, sizing_mode="stretch_both")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/jovyan/repos/private/panel/panel/dataclass.py", line 159, in __init__
super().__init__(**params)
File "/home/jovyan/repos/private/panel/panel/viewable.py", line 302, in __init__
super().__init__(**params)
File "/home/jovyan/repos/private/panel/panel/dataclass.py", line 106, in __init__
utils.sync_with_parameterized(self.model, self, names=names)
File "/home/jovyan/repos/private/panel/panel/_dataclasses/base.py", line 159, in sync_with_parameterized
setattr(parameterized, parameter, field_value)
File "/home/jovyan/repos/private/panel/.venv/lib/python3.11/site-packages/param/parameterized.py", line 528, in _f
instance_param.__set__(obj, val)
File "/home/jovyan/repos/private/panel/.venv/lib/python3.11/site-packages/param/parameterized.py", line 530, in _f
return f(self, obj, val)
^^^^^^^^^^^^^^^^^
File "/home/jovyan/repos/private/panel/.venv/lib/python3.11/site-packages/param/parameterized.py", line 1498, in __set__
self._validate(val)
File "/home/jovyan/repos/private/panel/.venv/lib/python3.11/site-packages/param/parameters.py", line 1192, in _validate
self._validate_length(val, self.length)
File "/home/jovyan/repos/private/panel/.venv/lib/python3.11/site-packages/param/parameters.py", line 1185, in _validate_length
raise ValueError(
ValueError: Attribute 'length' of Tuple parameter 'Map.bounds' is not of the correct length (0 instead of 2). |
FIXED Ahh. The dataclass functionality for Pydantic does not add specific parameter types yet. |
Fixed by adding Our code instantiates pydantic models. Often they don't have default values. Instead initial values are required. This makes it a bit hard to use our features. Especially for creating forms with validation which a popular use case (c.f. pydantic-panel, streamlit-pydantic, dash-pydantic-form). For example the below test will currently raise an exception def test_to_parameterized_no_defaults():
from pydantic import BaseModel
class ExampleModel(BaseModel):
some_text: str
some_number: int
class ExampleModelParameterized(ModelParameterized):
_model_class = ExampleModel
ExampleModelParameterized() Something like the import panel as pn
from pydantic import BaseModel
import param
from panel._dataclasses.pydantic import PydanticUtils
pn.extension()
class ModelForm(pn.viewable.Viewer):
value = param.ClassSelector(class_=BaseModel, allow_None=True)
submit_button_visible = param.Boolean(default=True, label="Show Submit Button")
def __init__(self, model_class, submit_button_visible: bool=True, **params):
self._model_class = model_class
self._fields = list(model_class.model_fields.keys())
super().__init__(**params)
fields = model_class.model_fields
default_values = {field: PydanticUtils.create_parameter(model_class, field).default for field, info in fields.items() if info.is_required()}
model=model_class(**default_values)
self._model = model=model_class(**default_values)
parameters = list(ExampleModel.model_fields.keys())
parameterized = pn.dataclass.to_viewer(model)
parameterized.param.watch(self._update_value_on_parameter_change, parameters)
submit = pn.widgets.Button(name="Submit", button_type="primary", on_click=self._update_value, visible=self.param.submit_button_visible)
self._form = pn.Column(
pn.Param(parameterized, parameters=parameters),
submit)
def _update_value(self, *args):
self.value = self._model.copy(deep=True)
def _update_value_on_parameter_change(self, *args):
if not self.submit_button_visible:
self.value = self._model.copy(deep=True)
def __panel__(self):
return self._form
@param.depends("value")
def value_as_dict(self):
if not self.value:
return {}
return self.value.dict()
class ExampleModel(BaseModel):
some_text: str
some_number: int
some_boolean: bool
form = ModelForm(model_class=ExampleModel)
pn.Column(form, pn.pane.JSON(form.value_as_dict), form.param.submit_button_visible).servable() |
return tuple(rx_values) | ||
|
||
|
||
class ModelForm(Viewer): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the ModelForm
to support the form use case. This is the use case that pydantic-panel, streamlit-pydantic and dash-pydantic-form all support.
Its really a more general request as in #3687. We can solve this is several ways:
- not solve :-)
- document how to solve
- make general Panel form functionality
- add this dataclass specific form functionality.
What do you recommend @philippjfr ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its worth noting that I achieve the form functionality a bit differently than the packages mentioned above. They translate pydantic fields to widgets while I translate pydantic fields to Parameters which Panel can translate to widgets. I.e. in my version we instantiate a Parameterized and then use Param. In the packages they can avoid instantiating a model before the user has filled out the form including required values.
Is my version the right way to go @philippjfr ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! And the form use case should definitely be supported.
I've tried to achieve almost feature parity with pydantic-panel. What is missing is Pydantic BaseModel attributes and pandas intervals.
pip install pydantic-panel import pydantic
import panel as pn
from typing import List
from pydantic_panel.dispatchers import infer_widget
from datetime import datetime, date
import numpy as np
import pandas as pd
pn.extension("tabulator")
class ChildModel(pydantic.BaseModel):
name: str = "child"
class SomeModel(pydantic.BaseModel):
name: str = "some model"
child_field: ChildModel = ChildModel()
date_field: date = date(2024,1,2)
dateframe: pd.DataFrame = pd.DataFrame({"x": [1], "y": ["a"]})
datetime_field: datetime = datetime(2024,1,1)
dict_field: dict = {"a": 1}
float_field: float = 42
int_field: int = pydantic.Field(default=2, lt=10, gt=0, multiple_of=2)
list_field: list = [1, "two"]
nparray_field: np.ndarray = np.array([1, 2, 3])
str_field: str = pydantic.Field(default = "to", min_length=2, max_length=10)
tuple_field: tuple = ("a", 1)
class Config:
arbitrary_types_allowed = True # to allow np.array
model = SomeModel()
pydantic_panel_editor = pn.panel(model, sizing_mode="fixed") # Pydantic(model).layout[0]
print(type(pydantic_panel_editor))
panel_editor = pn.Param(pn.dataclass.to_parameterized(model))
pn.Row(
pydantic_panel_editor,
panel_editor,
).servable() |
What else should be done here? |
I'm still fully on board with the aims of this PR but it's simply too large a PR to make it into 1.5.0 at this point. |
Superseedes #6892.
Also motivated by me trying to demonstrate that you can just as well use Panel for geospatial applications as Solara by creating apps similar to https://github.com/opengeos/solara-geospatial/tree/main/pages. But currently Panel is harder to use because it requires adding more code for using
observer
pattern.Scope: Currently ipywidgets, Pydantic models
Easy to view docs
Todo
panel.ipywidget
.dev
docs:TypeError: Cannot read properties of undefined (reading 'loader')
.self.param.add_parameter(parameter, param.Parameter())
create_parameter
of ipywidgets.create_parameter
for pydantic to add appropriate types of parameters.Maybe later
ModelForm
.Promotion
Note: Features have been moved to
panel.dataclass
module since this video was made.WidgetViewer
has been renamed toModelViewer
.wrapping_ipywidgets.mp4
Design Principles
ModelViewer
class andcreate_rx
function such that there are no dead ends and its testable.