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])