Skip to content

Commit

Permalink
Merge pull request #18 from trungleduc/ft/widget-factory
Browse files Browse the repository at this point in the history
Create widget from factory
  • Loading branch information
trungleduc committed Dec 14, 2021
2 parents 87f3974 + 553d420 commit 4ef8f0f
Show file tree
Hide file tree
Showing 15 changed files with 567 additions and 537 deletions.
496 changes: 30 additions & 466 deletions css/widget.css

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/source/examples/factory.nblink
@@ -0,0 +1,4 @@
{
"path": "../../../examples/widget_factory/widget_factory.ipynb",
"extra-media": ["../../../examples/widget_factory/"]
}
1 change: 1 addition & 0 deletions docs/source/examples/index.rst
Expand Up @@ -6,6 +6,7 @@ This section contains several examples generated from Jupyter notebooks.
The widgets have been embedded into the page for demonstrative purposes.

.. toctree::
:maxdepth: 1
:glob:

*
Binary file added docs/source/images/factory_widget.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 24 additions & 2 deletions docs/source/usage.rst
Expand Up @@ -2,12 +2,12 @@
Usage
=============

**ipyflex** is meant to be used with widgets based on `ipywidgets`_. The entry point of **ipyflex** is the `FlexLayout` class, it allows users to dynamically customize the layout and fill their dashboard from the existing widgets.
**ipyflex** is meant to be used with widgets based on `ipywidgets`_. The entry point of **ipyflex** is the *FlexLayout* class, it allows users to dynamically customize the layout and fill their dashboard from the existing widgets.

Create a dashboard from existing widgets
==========================================

The simplest way to create an **ipyflex** dashboard is to create a dictionary of existing widgets with the `keys` are the names of the widget and `values` are the instances of widgets and then use `FlexLayout` to compose the layout.
The simplest way to create an **ipyflex** dashboard is to create a dictionary of existing widgets with the `keys` are the names of the widget and `values` are the instances of widgets and then use *FlexLayout* to compose the layout.

.. code:: Python
Expand Down Expand Up @@ -36,6 +36,28 @@ Users can pass some configurations to the constructor of *FlexLayout* to set the
- *style*: CSS styles to be passed to the root element of the dashboard, it accepts any CSS rules but the keys need to be in *camelCase* format.
- *editable*: flag to enable or disable the editable mode. In non-editable mode, the toolbar with the *Save template* button is removed, tabs can not be removed, dragged, or renamed.

--------------------------------------
Create widgets from factory functions
--------------------------------------

In the case of using existing widgets in *FlexLayout* dashboard, users can create multiple views of a widget, so all tabs are linked. If users want to have the independent widget in each tab, *FlexLayout* allows users to define the factories to create widgets from the interface.

.. code:: Python
def slider_factory(label: 'Label of slider', value: 'Initial value'):
return ipywidgets.FloatSlider(value=float(value), description=label )
factories = {"Slider factory": slider_factory}
dashboard = FlexLayout(widgets, factories=factories)
.. image:: images/factory_widget.gif

If the factory function needs parameters, *FlexLayout* will build an input form to get parameters from the interface. Users can define annotations to have the label of the input form.

.. note::
*FlexLayout* will pass all parameters as string, users need to convert the inputs to their appropriate type in the factory function.

----------------------------
FlexLayout interface
----------------------------
Expand Down
135 changes: 135 additions & 0 deletions examples/widget_factory/widget_factory.ipynb
@@ -0,0 +1,135 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d4c569c9-9060-4245-abdf-3a434f2c3a67",
"metadata": {},
"source": [
"## A widget factory example"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "8e9ceab0-5b1d-413b-8744-bb3ea92dc6cb",
"metadata": {},
"outputs": [],
"source": [
"import ipywidgets\n",
"from ipyflex import FlexLayout"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "887ba9a0-a506-49ae-8324-aeb36535b791",
"metadata": {},
"outputs": [],
"source": [
"slider = ipywidgets.FloatSlider(description='Linked slider')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "94b10897-0011-4c76-b14f-663710581d05",
"metadata": {},
"outputs": [],
"source": [
"def slider_factory(label: 'Label of slider', value: 'Initial value'):\n",
" return ipywidgets.FloatSlider(value=float(value), description=label )"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "6b13a1d1-183f-478a-8ddc-f4ee44208efc",
"metadata": {},
"outputs": [],
"source": [
"widgets = {'Linked slider': slider}\n",
"factories = {\"Slider factory\": slider_factory}"
]
},
{
"cell_type": "markdown",
"id": "ac5f292c-581f-4ad7-9eb5-29b677278307",
"metadata": {},
"source": [
"### Create an empty dashboard with factory"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "05e001ae-28a9-410d-ba96-33aaa92576a2",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "11166ad0399740f8a64fc5e9b863f376",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"FlexLayout(children={'Linked slider': FloatSlider(value=0.0, description='Linked slider')}, layout_config={'bo…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"FlexLayout(widgets, factories=factories, style={'height':'300px'})"
]
},
{
"cell_type": "markdown",
"id": "e53da48e-56f5-4413-bc40-1e95588b74f9",
"metadata": {},
"source": [
"### Load dashboard from template"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ef565dce-efc6-4481-9ae1-6acd2257a01f",
"metadata": {},
"outputs": [],
"source": [
"FlexLayout(widgets, factories=factories, style={'height':'300px'}, template = 'widget_factory.json')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6219ba62-23b8-441f-843a-1e650243bba5",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
1 change: 1 addition & 0 deletions examples/widget_factory/widget_factory.json
@@ -0,0 +1 @@
{"global": {"tabSetEnableClose": true, "tabSetTabLocation": "bottom"}, "borders": [], "layout": {"type": "row", "id": "#1", "children": [{"type": "tabset", "id": "#2", "children": [{"type": "tab", "id": "#3", "name": "New section ", "component": "sub", "config": {"model": {"global": {"tabSetEnableClose": true}, "borders": [], "layout": {"type": "row", "id": "#1", "children": [{"type": "row", "id": "#1399287c-e9b4-493c-abb3-54168c374352", "weight": 50, "children": [{"type": "tabset", "id": "#d95f9720-fb3d-43bb-a10f-10a4c0a61807", "weight": 50, "children": [{"type": "tab", "id": "#6d9d8b09-f17b-43ef-bb53-fbf936f74da0", "name": "Linked slider", "component": "Widget", "config": {"layoutID": "#3"}}]}, {"type": "tabset", "id": "#061a7542-6b94-454e-bb90-7a6d0bed7f1e", "weight": 50, "children": [{"type": "tab", "id": "#f95061fa-87c5-476e-98a3-5e7bf447deba", "name": "Linked slider", "component": "Widget", "config": {"layoutID": "#3"}}]}]}, {"type": "row", "id": "#69568e98-a2e4-4643-8cf2-2b3451718b15", "weight": 50, "children": [{"type": "tabset", "id": "#fab8e962-3886-47c3-9697-9ce8472ae8fc", "weight": 50, "children": [{"type": "tab", "id": "#6823abc0-557b-4720-b99b-a093cdd2b3a4", "name": "Slider factory", "component": "Widget", "config": {"layoutID": "#3", "extraData": {"label": "Slider 1", "value": "10"}}}]}, {"type": "tabset", "id": "#f5e77e9e-863d-443c-88cf-fe5457874302", "weight": 50, "children": [{"type": "tab", "id": "#cfa489e0-249c-4fa7-bf15-0dc54619d8d9", "name": "Slider factory", "component": "Widget", "config": {"layoutID": "#3", "extraData": {"label": "Slider 2", "value": "20"}}}], "active": true}]}]}}}}], "active": true}]}}
91 changes: 77 additions & 14 deletions ipyflex/flex_layout.py
Expand Up @@ -4,28 +4,28 @@
# Copyright (c) Trung Le.
# Distributed under the terms of the Modified BSD License.

"""
TODO: Add module docstring
"""

import json
import os
from enum import Enum
from typing import Dict as TypeDict
from typing import Callable, Dict as TypeDict
from typing import List as TypeList
from typing import Union

from ipywidgets import DOMWidget, Widget, widget_serialization
from traitlets.traitlets import Bool, Dict, Instance, Unicode
from traitlets.traitlets import Bool, Dict, Instance, Unicode, List

from ._frontend import module_name, module_version
from .utils import get_nonexistant_path
from .utils import get_nonexistant_path, get_function_signature
import copy


class MESSAGE_ACTION(str, Enum):
SAVE_TEMPLATE = 'save_template'
UPDATE_CHILDREN = 'update_children'
REQUEST_FACTORY = 'request_factory'
RENDER_FACTORY = 'render_factory'
RENDER_ERROR = 'render_error'
ADD_WIDGET = 'add_widget'


class FlexLayout(DOMWidget):
Expand All @@ -39,15 +39,18 @@ class FlexLayout(DOMWidget):
_view_module = Unicode(module_name).tag(sync=True)
_view_module_version = Unicode(module_version).tag(sync=True)

# children = TypedTuple(
# trait=Instance(Widget), help="List of widget children"
# ).tag(sync=True, **widget_serialization)
children = Dict(
key_trait=Unicode,
value_trait=Instance(Widget),
help='Dict of widget children',
).tag(sync=True, **widget_serialization)

widget_factories = Dict(
key_trait=Unicode, value_trait=Dict, default_value={}
).tag(sync=True)

placeholder_widget = List(trait=Unicode, default_value=[]).tag(sync=True)

layout_config = Dict(
{'borderLeft': False, 'borderRight': False},
help='Layout configuration',
Expand All @@ -73,7 +76,7 @@ class FlexLayout(DOMWidget):
def __init__(
self,
widgets: Union[TypeDict, TypeList] = [],
# layout_config: TypeDict,
factories: TypeDict[str, Callable] = {},
**kwargs,
):
super().__init__(**kwargs)
Expand All @@ -86,9 +89,27 @@ def __init__(
}
else:
raise TypeError('Invalid input!')
if len(list(self.RESERVED_NAME & set(self.children))) > 0:
raise KeyError('Please do not use widget name in reserved list!')

children_set = set(self.children)
factories_set = set(factories)
if (
len(self.RESERVED_NAME & children_set) > 0
or len(self.RESERVED_NAME & factories_set) > 0
):
raise KeyError(
f'Please do not use widget name in reserved list: {self.RESERVED_NAME}'
)

if len(children_set & factories_set) > 0:
raise ValueError(
'Please do not use a same name for both widget and factory'
)
factories_sig = {}
for key, factory in factories.items():
factories_sig[key] = get_function_signature(factory)

self.widget_factories = factories_sig
self.placeholder_widget = []
self._factories = factories
self.template_json = None
if self.template is not None:
try:
Expand All @@ -107,6 +128,13 @@ def add(self, name: str, widget: Widget) -> None:
return
if name in self.RESERVED_NAME:
raise KeyError('Please do not use widget name in reserved list!')
error = (
lambda type: f'A {type} with the same name is already registered!'
)
if name in self.widget_factories:
raise KeyError(error('factory'))
if name in self.children:
raise KeyError(error('widget'))

old = copy.copy(self.children)
old[name] = widget
Expand Down Expand Up @@ -137,3 +165,38 @@ def _handle_frontend_msg(
with open(file_path, 'w') as f:
json.dump(json_data, f)
self.template = file_path
elif action == MESSAGE_ACTION.REQUEST_FACTORY:
factory_name = payload['factory_name']
uuid = payload['uuid']
if factory_name in self.widget_factories:
if 'extraData' in payload:
params = payload['extraData']
else:
params = {}
try:
w_model = self._factories[factory_name](**params)
model_msg = widget_serialization['to_json'](w_model, None)
self.send(
{
'action': MESSAGE_ACTION.RENDER_FACTORY,
'payload': {'model_id': model_msg, 'uuid': uuid},
}
)
except Exception as e:
self.send(
{
'action': MESSAGE_ACTION.RENDER_ERROR,
'payload': {'error_msg': str(e), 'uuid': uuid},
}
)

elif action == MESSAGE_ACTION.ADD_WIDGET:
widget_name = payload['name']
old = copy.copy(self.placeholder_widget)
if widget_name not in old:
old.append(widget_name)
self.placeholder_widget = old
else:
raise KeyError(
'A widget with the same name is already registered!'
)
18 changes: 17 additions & 1 deletion ipyflex/utils.py
@@ -1,7 +1,9 @@
import os
import inspect
from typing import Callable, Dict


def get_nonexistant_path(fname_path):
def get_nonexistant_path(fname_path) -> str:
if not os.path.exists(fname_path):
return fname_path
filename, extension = os.path.splitext(fname_path)
Expand All @@ -12,3 +14,17 @@ def get_nonexistant_path(fname_path):
i += 1
new_fname = get_name(i)
return new_fname


def get_function_signature(f: Callable) -> Dict:
sig = inspect.signature(f)

params = sig.parameters
ret = {}
for key in sig.parameters:
param = params.get(key)
ret[key] = {
'annotation': str(param.annotation) if param.annotation != inspect._empty
else None
}
return ret

0 comments on commit 4ef8f0f

Please sign in to comment.