Skip to content

Commit

Permalink
Merge pull request #118 from Suraj3620/unionfix
Browse files Browse the repository at this point in the history
Updated test logic and provided fix for unions and related issues
  • Loading branch information
dokempf authored Jan 22, 2025
2 parents 1588566 + f55e953 commit 096045a
Show file tree
Hide file tree
Showing 60 changed files with 1,563 additions and 92 deletions.
226 changes: 136 additions & 90 deletions ipywidgets_jsonschema/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def __init__(
self.time_parse_func = time_parse_func
self.show_descriptions = show_descriptions

self._construction_stack = []
# Store a list of registered observers to add them to runtime-generated widgets
self._observers = []

Expand Down Expand Up @@ -209,6 +210,10 @@ def data(self, _data):
self._form_element.setter(_data)

def _construct(self, schema, label=None, root=False):
if schema in self._construction_stack:
return self.construct_element()
self._construction_stack.append(schema)

# Enumerations are handled a dropdowns
if "enum" in schema:
return self._construct_enum(schema, label=label)
Expand All @@ -228,7 +233,11 @@ def _construct(self, schema, label=None, root=False):
# Handle other input based on the input type
type_ = schema.get("type", None)
if type_ is None:
raise FormError("Expecting type information for non-enum properties")
if "$ref" in schema:
return self._construct_ref(schema, label=label)
raise FormError(
f"Expecting type information for non-enum properties, schema: {schema}"
)
if not isinstance(type_, str):
raise FormError("Not accepting arrays of types currently")

Expand All @@ -240,7 +249,9 @@ def _construct(self, schema, label=None, root=False):
):
type_ = format_.replace("-", "_")

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

def _wrap_accordion(self, widget_list, schema, label=None):
titles = []
Expand Down Expand Up @@ -285,104 +296,116 @@ def _construct_object(self, schema, label=None, root=False):
# cprop: The property that is maybe added
# element: The subelement for the property
conditionals = []
for prop, subschema in schema["properties"].items():
elements[prop] = self._construct(subschema, label=prop)

# Add conditional elements
def add_conditional_elements(s):
# Check whether we have an if statement
cond = s.get("if", None)
if cond is None:
return

for cprop, csubschema in s.get("then", {}).get("properties", {}).items():
celem = self._construct(csubschema, label=cprop)
conditionals.append((cond, cprop, celem))
elements[cprop] = celem

if "else" in s:
add_conditional_elements(s["else"])

add_conditional_elements(schema)

# Apply sorting to the keys
keys = schema["properties"].keys()
try:
keys = self.sorter(keys)
except TypeError:
# If the keys cannot be compared, we stick to the original order
pass

# Collect the list of widgets: First the regular ones, then conditional ones
widget_list = sum((elements[k].widgets for k in keys), [])
widget_list.extend(
[
ipywidgets.HBox(layout=ipywidgets.Layout(width="100%"))
for _ in range(len(conditionals))
]
)

if "properties" in schema:
for prop, subschema in schema["properties"].items():
elements[prop] = self._construct(subschema, label=prop)

# Add conditional elements
def add_conditional_elements(s):
# Check whether we have an if statement
cond = s.get("if", None)
if cond is None:
return

for cprop, csubschema in (
s.get("then", {}).get("properties", {}).items()
):
celem = self._construct(csubschema, label=cprop)
conditionals.append((cond, cprop, celem))
elements[cprop] = celem

if "else" in s:
add_conditional_elements(s["else"])

add_conditional_elements(schema)

# Apply sorting to the keys
keys = schema["properties"].keys()
try:
keys = self.sorter(keys)
except TypeError:
# If the keys cannot be compared, we stick to the original order
pass

# Collect the list of widgets: First the regular ones, then conditional ones
widget_list = sum((elements[k].widgets for k in keys), [])
widget_list.extend(
[
ipywidgets.HBox(layout=ipywidgets.Layout(width="100%"))
for _ in range(len(conditionals))
]
)
else:
widget_list = []

# Maybe wrap this in an Accordion widget
wrapped_widget_list = widget_list
if not root and len(schema["properties"]) > 1:
if not root and len(schema.get("properties", {})) > 1:
wrapped_widget_list = self._wrap_accordion(widget_list, schema, label=label)

def _getter():
# Get all regular properties
result = {}
for k in schema["properties"].keys():
result[k] = elements[k].getter()
if "properties" in schema:
for k in schema["properties"].keys():
result[k] = elements[k].getter()

# Add conditional properties
for cschema, cprop, celem in conditionals:
try:
jsonschema.validate(instance=result, schema=cschema)
result[cprop] = celem.getter()
except jsonschema.ValidationError:
pass
# Add conditional properties
for cschema, cprop, celem in conditionals:
try:
jsonschema.validate(instance=result, schema=cschema)
result[cprop] = celem.getter()
except jsonschema.ValidationError:
pass
elif "additionalProperties" in schema:
result = {}

return result

def _setter(_d):
for k in elements.keys():
if k in _d:
elements[k].setter(_d[k])
else:
elements[k].resetter()
if "properties" in schema:
for k in elements.keys():
if k in _d:
elements[k].setter(_d[k])
else:
elements[k].resetter()

def _register_observer(h, n, t):
for e in elements.values():
e.register_observer(h, n, t)
if "properties" in schema:
for e in elements.values():
e.register_observer(h, n, t)

def _resetter():
for e in elements.values():
e.resetter()
if "properties" in schema:
for e in elements.values():
e.resetter()

# Add the conditional information
for i, (cschema, cprop, celem) in enumerate(conditionals):

def create_observer(j, s, prop, e):
def _cond_observer(_):
# Check whether our data matches the given schema
try:
jsonschema.validate(instance=_getter(), schema=s)
elements[prop] = e
widget_list[len(keys) + j].children = e.widgets
except jsonschema.ValidationError:
widget_list[len(keys) + j].children = []

# We need to call the observer once so that we get a correctly
# initialized widget, because otherwise it triggers only if it
# differs from the default.
_cond_observer({})

return _cond_observer

for k in cschema.get("properties", {}).keys():
elements[k].register_observer(
create_observer(i, cschema, cprop, celem), "value", "change"
)

if "properties" in schema:
for i, (cschema, cprop, celem) in enumerate(conditionals):

def create_observer(j, s, prop, e):
def _cond_observer(_):
# Check whether our data matches the given schema
try:
jsonschema.validate(instance=_getter(), schema=s)
elements[prop] = e
widget_list[len(keys) + j].children = e.widgets
except jsonschema.ValidationError:
widget_list[len(keys) + j].children = []

# We need to call the observer once so that we get a correctly
# initialized widget, because otherwise it triggers only if it
# differs from the default.
_cond_observer({})

return _cond_observer

for k in cschema.get("properties", {}).keys():
elements[k].register_observer(
create_observer(i, cschema, cprop, celem), "value", "change"
)
# Ensure that defaults are initialized
_resetter()

Expand Down Expand Up @@ -890,10 +913,11 @@ def _move(b):
up.on_click(move(-1))
down.on_click(move(1))

# Construct the final widget including array controls
children = [
recelem.widgets[0].children[0],
]
children = []
if recelem.widgets and recelem.widgets[0].children:
children = [
recelem.widgets[0].children[0],
]
if not fixed_length:
children.append(
ipywidgets.HBox(
Expand Down Expand Up @@ -1006,14 +1030,28 @@ def _construct_anyof(self, schema, label=None, key="anyOf"):
elements = []

# Iterate over the given subschema
for s in schema[key]:
for i, s in enumerate(schema[key]):

if "title" in s:
names.append(s["title"])
elements.append(self._construct(s))
elif "$ref" in s:
names.append(s["$ref"].split("/")[-1])
elif "type" in s:
if (
s["type"] == "object"
and "properties" in s
and "title" in s["properties"]
):
names.append(s["properties"]["title"]["const"])
else:
names.append(s["type"])
elif "additionalProperties" in s and "title" in schema:
names.append(f"Option {len(names) + 1}")

else:
raise FormError(
"Schemas within anyOf/oneOf/allOf need to set the title field"
)
names.append(f"Option {len(names) + 1}")

elements.append(self._construct(s))

# Create the selector and subschema widget
selector = ipywidgets.Dropdown(options=names, value=names[0])
Expand All @@ -1032,12 +1070,16 @@ def _setter(_d):
selector.value = names[i]
_select(None)
elements[i].setter(_d)
return # Exit if one schema matches

except jsonschema.ValidationError:
pass

def _resetter():
for e in elements:
e.resetter()
if "default" in schema and schema["default"] is not None:
_setter(schema["default"])

def _register_observer(h, n, t):
selector.observe(h, names=n, type=t)
Expand All @@ -1053,6 +1095,10 @@ def _register_observer(h, n, t):
register_observer=_register_observer,
)

def _construct_ref(self, schema, label=None):
ref = schema["$ref"].split("/")[-1]
return self._construct(self.schema["$defs"][ref], label=label)


def deep_update_missing(target, source):
target = target.copy()
Expand Down
33 changes: 33 additions & 0 deletions schemas/basictypes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"properties": {
"bool_field": {
"title": "Bool Field",
"type": "boolean"
},
"float_field": {
"title": "Float Field",
"type": "number"
},
"int_field": {
"title": "Int Field",
"type": "integer"
},
"none_field": {
"title": "None Field",
"type": "null"
},
"string_field": {
"title": "String Field",
"type": "string"
}
},
"required": [
"string_field",
"int_field",
"float_field",
"bool_field",
"none_field"
],
"title": "BasicTypes",
"type": "object"
}
26 changes: 26 additions & 0 deletions schemas/category.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$defs": {
"Category": {
"properties": {
"name": {
"title": "Name",
"type": "string"
},
"subcategories": {
"items": {
"$ref": "#/$defs/Category"
},
"title": "Subcategories",
"type": "array"
}
},
"required": [
"name",
"subcategories"
],
"title": "Category",
"type": "object"
}
},
"$ref": "#/$defs/Category"
}
Loading

0 comments on commit 096045a

Please sign in to comment.