-
-
Notifications
You must be signed in to change notification settings - Fork 551
Make it easy to use dataclass like models using familiar apis #6912
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
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. |
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. |
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.
Amusingly, given the PR title and module name, I was expecting support for Python's stdlib dataclass
:)
|
||
The `_model_names` attribute is an optional iterable or dictionary. It specifies which traits to synchronize to which parameters. | ||
|
||
## Create Reactive Values from the Traits of an ipywidget |
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 think we use reactive expressions in the documentation elsewhere (panel, param).
@@ -155,6 +155,7 @@ polars = "*" | |||
reacton = "*" | |||
scipy = "*" | |||
textual = "*" | |||
pydantic = "*" |
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.
It's best to add a minimum pin, as the code is likely not to be compatible with all the versions of pydantic. And yes, most of the dependencies there have no pins, it doesn't mean that's the right way :)
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.