Source code for paidiverpy.frontend.render
"""WidgetRenderer class for rendering widgets based on configuration parameters."""
import types
from typing import Literal
from typing import get_args
import panel as pn
from paidiverpy.frontend.parse import define_default_value
from paidiverpy.frontend.parse import parse_default_params
from paidiverpy.models.colour_params import * # noqa: F403
from paidiverpy.models.colour_params import COLOUR_LAYER_METHODS
from paidiverpy.models.convert_params import * # noqa: F403
from paidiverpy.models.convert_params import CONVERT_LAYER_METHODS
from paidiverpy.models.custom_params import * # noqa: F403
from paidiverpy.models.general_config import GeneralConfig # noqa: F401
from paidiverpy.models.open_params import * # noqa: F403
from paidiverpy.models.position_params import * # noqa: F403
from paidiverpy.models.position_params import POSITION_LAYER_METHODS
from paidiverpy.models.sampling_params import * # noqa: F403
from paidiverpy.models.sampling_params import SAMPLING_LAYER_METHODS
from paidiverpy.models.step_config import ColourConfig # noqa: F401
from paidiverpy.models.step_config import ConvertConfig # noqa: F401
from paidiverpy.models.step_config import CustomConfig # noqa: F401
from paidiverpy.models.step_config import PositionConfig # noqa: F401
from paidiverpy.models.step_config import SamplingConfig # noqa: F401
from paidiverpy.utils.base_model import BaseModel
OPTIONAL = 2
[docs]
class WidgetRenderer:
"""Class for rendering widgets based on configuration parameters.
Args:
steps (bool): If True, the renderer will handle step-specific widgets.
step_parameters (dict, optional): Parameters for the step if applicable.
"""
def __init__(self, steps: bool = False, step_parameters: BaseModel | None = None):
self.steps = steps
if step_parameters is not None:
self.step_class = step_parameters.__class__.__name__
self.step_parameters = step_parameters.to_dict()
else:
self.step_class = None
self.step_parameters = None
[docs]
def create_widget(self, name: str, field: dict, html_h_tag: int = 2) -> pn.Column:
"""Create a widget based on the field type and name.
Args:
name (str): The name of the field.
field (dict): The field definition containing type, description, default value, etc.
html_h_tag (int): The HTML heading tag level for the title.
Returns:
pn.Column: A Panel Column containing the title and the input widget.
"""
model_class = globals().get(field["type"])
if model_class and hasattr(model_class, "model_config") and model_class.model_config.get("extra") == "allow":
default = {}
for local_name, local_field in model_class.model_fields.items():
default[local_name] = define_default_value(local_field)
field = {"default": default, "description": model_class.__doc__, "type": "dict"}
type_ = field["type"]
description = field.get("description", "")
default = field.get("default")
name_title = name.replace("_", " ").capitalize()
html_pane = pn.pane.HTML(
f"<div class='ppy-pn-w-50'><div class='ppy-pn-title-{html_h_tag} ppy-pn-bold'>{name_title}</div>"
f"<div class='ppy-pn-description'>{description}</div></div>"
)
input_widget = self.get_input_widget(type_.lower(), field, name, default, html_h_tag)
if input_widget:
return pn.Column(
html_pane,
input_widget,
styles={"padding": "0px"},
)
if not input_widget and type_.lower() == "union":
options = list(field["field_options"].keys())
if len(options) == OPTIONAL and "NoneType" in options:
input_widget = self.create_optional_widget(name, field, options, default, html_h_tag=html_h_tag)
else:
if "NoneType" in options:
options[options.index("NoneType")] = "Not provided (NoneType)"
if default is None:
default_type = "Not provided (NoneType)"
else:
default_type = next((k for k in options if k.lower() in str(type(default)).lower()), options[0])
if self.step_parameters and "steps[" in name:
selector = pn.widgets.Select(name=f"{name} type_selector", options=options, value=self.step_class)
else:
selector = pn.widgets.Select(name=f"{name} type_selector", options=options, value=default_type)
input_widget = pn.Column(
selector,
pn.bind(self.render_union_input, selector, field=field, name=name, default=default, html_h_tag=html_h_tag),
)
if input_widget:
return pn.Column(html_pane, input_widget, styles={"padding": "0px"})
return self.render_custom_types(model_class=type_, prefix=name, html_h_tag=html_h_tag)
[docs]
def create_optional_widget(
self, name: str, field: dict, options: list[str], default: str | float | bool | None = None, html_h_tag: int = 2
) -> pn.Column:
"""Create a widget for an optional field.
Args:
name (str): The name of the field.
field (dict): The field definition containing type, description, default value, etc.
options (list[str]): The options for the union type.
default (str | float | bool | None): The default value for the field.
html_h_tag (int): The HTML heading tag level for the title.
Returns:
pn.Column: A Panel Column containing the title and the input widget.
"""
other_type = next(opt for opt in options if opt != "NoneType")
if self.step_parameters:
new_names = name.split(".")[1:]
value = self.step_parameters.copy()
for new_name in new_names:
value = value.get(new_name)
if value is not None:
default = True
provide_checkbox = pn.widgets.Checkbox(name=f"Provide {name}?", value=default is not None)
return pn.Column(
provide_checkbox,
pn.bind(self.render_union_input, other_type, field=field, name=name, default=default, provide=provide_checkbox, html_h_tag=html_h_tag),
)
[docs]
def render_custom_types(self, model_class: str, prefix: str | None = None, html_h_tag: int = 2) -> pn.Column:
"""Render custom types based on the model class and field definition.
Args:
model_class (str): The name of the model class.
field (dict, optional): The field definition if applicable.
prefix (str, optional): The prefix for the field name.
html_h_tag (int): The HTML heading tag level for the title.
Returns:
pn.Column: A Panel Column containing the rendered widgets.
"""
model_class = globals().get(model_class)
field_meta = parse_default_params(model_class, self.steps)
if "mode" in field_meta and "params" in field_meta:
new_field_meta = {k: v for k, v in field_meta.items() if k not in ["mode", "params"]}
widgets = []
if new_field_meta:
html_h_tag = html_h_tag + 1
for field_name, field in new_field_meta.items():
full_name = f"{prefix}.{field_name}" if prefix else field_name
widget = self.create_widget(full_name, field, html_h_tag=html_h_tag)
widgets.append(widget)
widgets = self.render_method_with_mode_params(field_meta, model_class, prefix, html_h_tag, widgets=widgets)
else:
widgets = []
html_h_tag = html_h_tag + 1
for field_name, field in field_meta.items():
full_name = f"{prefix}.{field_name}" if prefix else field_name
widget = self.create_widget(full_name, field, html_h_tag=html_h_tag)
widgets.append(widget)
return pn.Column(*widgets, styles={"padding": "0px"})
[docs]
def render_list_input(self, field: dict, name: str, html_h_tag: int = 2) -> pn.Column:
"""Render a list input widget based on the field definition.
Args:
field (dict): The field definition containing type, description, default value, etc.
name (str): The name of the field.
html_h_tag (int): The HTML heading tag level for the title.
Returns:
pn.Column: A Panel Column containing the list input widget.
"""
item_type = field.get("item_type", "str") if "field_options" not in field else field["field_options"]["list"]
new_html_h_tag = html_h_tag + 1
inputs = []
inputs_container = pn.Column()
def create_list_widget(index: int) -> tuple[pn.pane.HTML, dict]:
html_pane = pn.pane.HTML(f"<div class='ppy-pn-title-{html_h_tag} ppy-pn-italic'>ITEM {index + 1}</div>")
if isinstance(item_type, types.UnionType):
field_def = {"type": "union", "field_options": {}}
for item in item_type.__args__:
field_def["field_options"][item.__name__] = item.model_json_schema()["description"]
else:
field_def = {"type": item_type.__name__}
widget = self.create_widget(f"{name}[{index + 1}]", field_def, html_h_tag=new_html_h_tag)
return html_pane, widget
def create_row(index: int, widget: dict) -> pn.Row:
remove_btn = pn.widgets.Button(name="Remove", button_type="danger", width=80)
def remove_item(event: pn.widgets.Button) -> None: # noqa: ARG001
inputs.pop(index)
inputs_container[:] = [create_row(i, item) for i, item in enumerate(inputs)]
remove_btn.on_click(remove_item)
if self.steps:
return pn.Row(
widget["widget"],
)
return pn.Row(pn.Column(widget["html"], widget["widget"]), remove_btn)
def add_item(event: pn.widgets.Button = None) -> None: # noqa: ARG001
html, widget = create_list_widget(len(inputs))
if self.steps:
inputs.append({"widget": widget})
else:
inputs.append({"html": html, "widget": widget})
inputs_container[:] = [create_row(i, item) for i, item in enumerate(inputs)]
add_item(None)
if self.steps:
return pn.Column(
pn.layout.Divider(),
inputs_container,
pn.layout.Divider(),
)
add_button_top = pn.widgets.Button(name="Add Item", button_type="primary")
add_button_bottom = pn.widgets.Button(name="Add Item", button_type="primary")
add_button_top.on_click(add_item)
add_button_bottom.on_click(add_item)
return pn.Column(
pn.Row(add_button_top),
pn.layout.Divider(),
inputs_container,
pn.layout.Divider(),
pn.Row(add_button_bottom),
)
[docs]
def render_union_input(
self, selected_type: str, field: dict, name: str, default: str | float | bool | None = None, provide: bool = True, html_h_tag: int = 2
) -> pn.widgets.Widget:
"""Render the input widget for a union type field.
Args:
selected_type (str): The selected type from the union.
field (dict): The field definition containing type, description, default value, etc.
name (str): The name of the field.
default (str | float | bool | None): The default value for the field.
provide (bool): Whether to provide the input widget or not.
html_h_tag (int): The HTML heading tag level for the title.
Returns:
pn.widgets.Widget: The input widget for the selected type.
"""
html_h_tag = html_h_tag + 1
input_widget = self.get_input_widget(selected_type, field, name, default, html_h_tag, provide=provide)
if not input_widget:
input_widget = self.render_custom_types(model_class=selected_type, prefix=name, html_h_tag=html_h_tag)
return input_widget
def _get_default_from_step_parameters(self, default: str | float | bool | None, name: str) -> str | float | bool | None:
if self.step_parameters:
new_names = name.split(".")[1:]
value = self.step_parameters.copy()
for new_name in new_names:
value = value.get(new_name)
if value is not None:
default = value
return default
[docs]
def get_input_widget( # noqa: C901
self,
type_: str,
field: dict,
name: str,
default: str | float | bool | None = None,
html_h_tag: int = 2,
provide: bool = True,
) -> pn.widgets.Widget | None:
"""Get the input widget based on the type and field definition.
Args:
type_ (str): The type of the field.
field (dict): The field definition containing type, description, default value, etc.
name (str): The name of the field.
default (str | float | bool | None): The default value for the field.
html_h_tag (int): The HTML heading tag level for the title.
provide (bool): Whether to provide the input widget or not.
Returns:
pn.widgets.Widget | None: The input widget for the field, or None if not applicable.
"""
result = None
selected_type = type_.lower()
default = self._get_default_from_step_parameters(default, name)
if not provide:
result = pn.widgets.TextInput(value="", disabled=True)
elif selected_type == "int":
result = pn.widgets.IntInput(name=name, value=default)
elif selected_type == "float":
result = pn.widgets.FloatInput(name=name, value=default)
elif selected_type == "bool":
result = pn.widgets.Checkbox(name=name, value=default)
elif selected_type == "tuple":
default = (1, 2) if default is None else tuple(default) if isinstance(default, list) else default
result = pn.widgets.TextInput(name=name, value=str(default), placeholder="(item1, item2, ...)")
elif selected_type == "dict":
default = {"key": "value"} if default is None or default == {} else default
result = pn.widgets.JSONEditor(name=name, value=default, width=400, mode="tree")
elif selected_type == "str":
result = pn.widgets.TextInput(name=name, value=default)
elif selected_type == "not provided (nonetype)":
result = pn.pane.HTML("<div class='ppy-pn-description ppy-pn-italic'>No input needed</div>")
elif selected_type in ["literal"]:
opts = field["field_options"].get("literal", []) if "field_options" in field else field.get("options", [])
result = pn.widgets.Select(name=name, options=opts, value=default)
elif selected_type == "list":
item_type = field.get("item_type", "str")
item_default = field.get("item_default", "")
result = self.render_list_input(field, name, html_h_tag=html_h_tag) if item_type else pn.widgets.TextInput(name=name, value=item_default)
return result
[docs]
def render_method_with_mode_params(
self, field_meta: dict, model_class: str, prefix: str | None = None, html_h_tag: int = 2, widgets: list[pn.Column] | None = None
) -> list[pn.Column]:
"""Render the method with mode and parameters based on the field metadata.
Args:
field_meta (dict): The field metadata containing mode and parameters.
model_class (str): The name of the model class.
prefix (str | None): The prefix for the field name.
html_h_tag (int): The HTML heading tag level for the title.
widgets (list[pn.Column] | None): Existing widgets to append to.
Returns:
list[pn.Column]: A list of Panel Columns containing the rendered widgets.
"""
mode_options = field_meta["mode"]["options"]
mode_select = pn.widgets.Select(
name=f"{prefix}.mode",
options=mode_options,
value=mode_options[0],
)
def get_param_model(model_class: str) -> dict:
mapping = {
"SamplingConfig": SAMPLING_LAYER_METHODS,
"PositionConfig": POSITION_LAYER_METHODS,
"ColourConfig": COLOUR_LAYER_METHODS,
"ConvertConfig": CONVERT_LAYER_METHODS,
}
return mapping.get(model_class)
def make_params_widget(selected_mode: str) -> pn.Column:
param_model_name = get_param_model(model_class.__name__)[selected_mode]["params"]
if param_model_name:
return self.render_custom_types(model_class=param_model_name.__name__, prefix=f"{prefix}.params", html_h_tag=html_h_tag + 1)
return pn.pane.Markdown(f"*No parameters available for mode `{selected_mode}`*")
params_widget = pn.bind(make_params_widget, mode_select)
if widgets is None:
widgets = []
widgets.extend(
[
pn.Column(
pn.pane.HTML(
f"<div class='ppy-pn-title-{html_h_tag} ppy-pn-bold'>Mode</div>"
f"<div class='ppy-pn-description'>{field_meta['mode'].get('description', '')}</div>"
),
mode_select,
pn.pane.HTML(
f"<div class='ppy-pn-title-{html_h_tag} ppy-pn-bold'>Params</div>"
f"<div class='ppy-pn-description'>{field_meta['params'].get('description', '')}</div>"
),
params_widget,
)
]
)
return widgets
[docs]
def widget_from_literal(self, field_name: str, literal_type: Literal["option1", "option2", "option3"]) -> pn.widgets.Select:
"""Create a widget from a literal type.
Args:
field_name (str): The name of the field.
literal_type (Literal): The literal type to create the widget from.
Returns:
pn.widgets.Select: A Panel Select widget with options from the literal type.
"""
options = list(get_args(literal_type))
return pn.widgets.Select(name=field_name, options=options, value=options[0])