Skip to content
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

add ability to interpret json schema format #85

Merged
merged 4 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions ipywidgets_jsonschema/form.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime

from IPython.display import display
from packaging import version

Expand Down Expand Up @@ -74,6 +76,12 @@ def __init__(
use_sliders=False,
preconstruct_array_items=0,
sorter=sorted,
date_time_fmt_func=lambda dt: dt.isoformat(),
date_time_parse_func=datetime.datetime.fromisoformat,
date_fmt_func=lambda dt: dt.isoformat(),
date_parse_func=datetime.date.fromisoformat,
time_fmt_func=lambda dt: dt.isoformat(),
time_parse_func=datetime.time.fromisoformat,
):
"""Create a form with Jupyter widgets from a JSON schema

Expand Down Expand Up @@ -108,6 +116,12 @@ def __init__(
self.use_sliders = use_sliders
self.preconstruct_array_items = preconstruct_array_items
self.sorter = sorter
self.date_time_fmt_func = date_time_fmt_func
self.date_time_parse_func = date_time_parse_func
self.date_fmt_func = date_fmt_func
self.date_parse_func = date_parse_func
self.time_fmt_func = time_fmt_func
self.time_parse_func = time_parse_func

# Store a list of registered observers to add them to runtime-generated widgets
self._observers = []
Expand Down Expand Up @@ -197,6 +211,21 @@ def _construct(self, schema, label=None, root=False):
raise FormError("Expecting type information for non-enum properties")
if not isinstance(type_, str):
raise FormError("Not accepting arrays of types currently")

format_ = schema.get("format", None)
available_fmt = []
if hasattr(ipywidgets, "DatetimePicker"):
available_fmt.append("date-time")
if hasattr(ipywidgets, "DatePicker"):
available_fmt.append("date")
if hasattr(ipywidgets, "TimePicker"):
available_fmt.append("time")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the rationale behind these checks? Have the relevant widgets been introduced very recently into ipywidgets?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they were added in 8. The test suite also runs agains 7, so in order to pass the tests I had to add these checks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I have slightly rewritten this in #87, as I would like to avoid doing to much hasattr stuff.


if format_ in available_fmt:
return getattr(self, f"_construct_{format_.replace('-','_')}")(
schema, label=label, root=root
)

return getattr(self, f"_construct_{type_}")(schema, label=label, root=root)

def _wrap_accordion(self, widget_list, schema, label=None):
Expand Down Expand Up @@ -435,6 +464,183 @@ def _getter():
def _construct_string(self, schema, label=None, root=False):
return self._construct_simple(schema, ipywidgets.Text(), label=label)

def _construct_date_time(self, schema, label=None, root=False):
widget = ipywidgets.DatetimePicker()

# Extract the best description that we have
tooltip = schema.get("description", None)

# Construct the label widget that describes the input
box = [widget]
if label is not None or "title" in schema:
# Extract the best guess for a title that we have
title = schema.get("title", label)

# Use the label as the backup tooltip
if tooltip is None:
tooltip = title

widget.description = title

# Make sure that the widget shows the tooltip
if tooltip is not None:
widget.tooltip = tooltip

def _register_observer(h, n, t):
widget.observe(h, names=n, type=t)

def _setter(_d):
widget.value = self.date_time_parse_func(_d)

def _resetter():
# Apply a potential default
if "default" in schema:
widget.value = self.date_time_parse_func(schema["default"])
else:
widget.value = datetime.datetime.now()

def _getter():
if widget.value:
return self.date_time_fmt_func(widget.value)
return ""

# Trigger generation of defaults in construction
_resetter()

# Make sure the widget adapts to the outer layout
widget.layout = ipywidgets.Layout(width="100%")

# Make the placing of labels optional
box_type = ipywidgets.HBox
if self.vertically_place_labels:
box_type = ipywidgets.VBox

return self.construct_element(
getter=_getter,
setter=_setter,
resetter=_resetter,
widgets=[box_type(box, layout=ipywidgets.Layout(width="100%"))],
register_observer=_register_observer,
)

def _construct_date(self, schema, label=None, root=False):
widget = ipywidgets.DatePicker()

# Extract the best description that we have
tooltip = schema.get("description", None)

# Construct the label widget that describes the input
box = [widget]
if label is not None or "title" in schema:
# Extract the best guess for a title that we have
title = schema.get("title", label)

# Use the label as the backup tooltip
if tooltip is None:
tooltip = title

widget.description = title

# Make sure that the widget shows the tooltip
if tooltip is not None:
widget.tooltip = tooltip

def _register_observer(h, n, t):
widget.observe(h, names=n, type=t)

def _setter(_d):
widget.value = self.date_parse_func(_d)

def _resetter():
# Apply a potential default
if "default" in schema:
widget.value = self.date_parse_func(schema["default"])
else:
widget.value = datetime.datetime.now()

def _getter():
if widget.value:
return self.date_fmt_func(widget.value)
return ""

# Trigger generation of defaults in construction
_resetter()

# Make sure the widget adapts to the outer layout
widget.layout = ipywidgets.Layout(width="100%")

# Make the placing of labels optional
box_type = ipywidgets.HBox
if self.vertically_place_labels:
box_type = ipywidgets.VBox

return self.construct_element(
getter=_getter,
setter=_setter,
resetter=_resetter,
widgets=[box_type(box, layout=ipywidgets.Layout(width="100%"))],
register_observer=_register_observer,
)

def _construct_time(self, schema, label=None, root=False):
widget = ipywidgets.TimePicker()

# Extract the best description that we have
tooltip = schema.get("description", None)

# Construct the label widget that describes the input
box = [widget]
if label is not None or "title" in schema:
# Extract the best guess for a title that we have
title = schema.get("title", label)

# Use the label as the backup tooltip
if tooltip is None:
tooltip = title

widget.description = title

# Make sure that the widget shows the tooltip
if tooltip is not None:
widget.tooltip = tooltip

def _register_observer(h, n, t):
widget.observe(h, names=n, type=t)

def _setter(_d):
widget.value = self.time_parse_func(_d)

def _resetter():
# Apply a potential default
if "default" in schema:
widget.value = self.time_parse_func(schema["default"])
else:
widget.value = datetime.datetime.now()

def _getter():
if widget.value:
return self.time_fmt_func(widget.value)
return ""

# Trigger generation of defaults in construction
_resetter()

# Make sure the widget adapts to the outer layout
widget.layout = ipywidgets.Layout(width="100%")

# Make the placing of labels optional
box_type = ipywidgets.HBox
if self.vertically_place_labels:
box_type = ipywidgets.VBox

return self.construct_element(
getter=_getter,
setter=_setter,
resetter=_resetter,
widgets=[box_type(box, layout=ipywidgets.Layout(width="100%"))],
register_observer=_register_observer,
)

def _construct_number(self, schema, label=None, root=False):
kwargs = dict()
if "multipleOf" in schema:
Expand Down
11 changes: 11 additions & 0 deletions tests/schemas/string-date.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"default": "2018-11-13",
"schema": {
"default": "2018-11-13",
"format": "date",
"type": "string"
},
"valid": [
"2018-11-13"
]
}
11 changes: 11 additions & 0 deletions tests/schemas/string-datetime.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"default": "2018-11-13T20:20:39+00:00",
"schema": {
"default": "2018-11-13T20:20:39+00:00",
"format": "date-time",
"type": "string"
},
"valid": [
"2018-11-13T20:20:39+00:00"
]
}
11 changes: 11 additions & 0 deletions tests/schemas/string-time.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"default": "20:20:39",
"schema": {
"default": "20:20:39",
"format": "time",
"type": "string"
},
"valid": [
"20:20:39"
]
}