Skip to content

Commit db53900

Browse files
authored
Merge pull request #57 from ssciwr/avoid-property-name-clashes
Allow multiple conditionals to define the same property
2 parents 3c13fcb + 1d900df commit db53900

File tree

4 files changed

+117
-50
lines changed

4 files changed

+117
-50
lines changed

ipywidgets_jsonschema/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from ipywidgets_jsonschema.form import Form
22

3-
__version__ = "0.11.0"
3+
__version__ = "0.11.1"

ipywidgets_jsonschema/form.py

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -228,37 +228,31 @@ def _construct_object(self, schema, label=None, root=False):
228228
elements = {}
229229

230230
# Store the conditional information from the schema in the following form:
231-
# cprop -> (schema, widget)
231+
# [(schema, cprop, element), ..]
232232
# with the following meaning:
233-
# cprop: The property that is maybe added
234233
# schema: The schema that the data needs to match
235-
# widget: The widget that implements the form
236-
conditionals = {}
234+
# cprop: The property that is maybe added
235+
# element: The subelement for the property
236+
conditionals = []
237237
for prop, subschema in schema["properties"].items():
238238
elements[prop] = self._construct(subschema, label=prop)
239239

240-
# Add conditional elements
241-
def add_conditional_elements(s):
242-
# Check whether we have an if statement
243-
cond = s.get("if", None)
244-
if cond is None:
245-
return
240+
# Add conditional elements
241+
def add_conditional_elements(s):
242+
# Check whether we have an if statement
243+
cond = s.get("if", None)
244+
if cond is None:
245+
return
246246

247-
for cprop, csubschema in (
248-
s.get("then", {}).get("properties", {}).items()
249-
):
250-
celem = self._construct(csubschema, label=cprop)
251-
cwidgets = celem.widgets[0].children
252-
celem.widgets[:] = [
253-
ipywidgets.HBox(layout=ipywidgets.Layout(width="100%"))
254-
]
255-
elements[cprop] = celem
256-
conditionals[cprop] = [cond, cwidgets]
247+
for cprop, csubschema in s.get("then", {}).get("properties", {}).items():
248+
celem = self._construct(csubschema, label=cprop)
249+
conditionals.append((cond, cprop, celem))
250+
elements[cprop] = celem
257251

258-
if "else" in s:
259-
add_conditional_elements(s["else"])
252+
if "else" in s:
253+
add_conditional_elements(s["else"])
260254

261-
add_conditional_elements(schema)
255+
add_conditional_elements(schema)
262256

263257
# Apply sorting to the keys
264258
keys = schema["properties"].keys()
@@ -271,29 +265,16 @@ def add_conditional_elements(s):
271265
# Collect the list of widgets: First the regular ones, then conditional ones
272266
widget_list = sum((elements[k].widgets for k in keys), [])
273267
widget_list.extend(
274-
sum((e.widgets for k, e in elements.items() if k not in keys), [])
268+
[
269+
ipywidgets.HBox(layout=ipywidgets.Layout(width="100%"))
270+
for _ in range(len(conditionals))
271+
]
275272
)
276-
if not root:
277-
widget_list = self._wrap_accordion(widget_list, schema, label=label)
278273

279-
# Add the conditional information
280-
for cprop, (cschema, cwidgets) in conditionals.items():
281-
282-
def create_observer(prop, s, w):
283-
def _cond_observer(_):
284-
# Check whether our data matches the given schema
285-
try:
286-
jsonschema.validate(instance=_getter(), schema=s)
287-
elements[prop].widgets[0].children = w
288-
except jsonschema.ValidationError:
289-
elements[prop].widgets[0].children = []
290-
291-
return _cond_observer
292-
293-
for k in cschema.get("properties", {}).keys():
294-
elements[k].register_observer(
295-
create_observer(cprop, cschema, cwidgets), "value", "change"
296-
)
274+
# Maybe wrap this in an Accordion widget
275+
wrapped_widget_list = widget_list
276+
if not root:
277+
wrapped_widget_list = self._wrap_accordion(widget_list, schema, label=label)
297278

298279
def _getter():
299280
# Get all regular properties
@@ -302,10 +283,10 @@ def _getter():
302283
result[k] = elements[k].getter()
303284

304285
# Add conditional properties
305-
for cprop, (cschema, _) in conditionals.items():
286+
for cschema, cprop, celem in conditionals:
306287
try:
307288
jsonschema.validate(instance=result, schema=cschema)
308-
result[cprop] = elements[cprop].getter()
289+
result[cprop] = celem.getter()
309290
except jsonschema.ValidationError:
310291
pass
311292

@@ -326,13 +307,39 @@ def _resetter():
326307
for e in elements.values():
327308
e.resetter()
328309

310+
# Add the conditional information
311+
for i, (cschema, cprop, celem) in enumerate(conditionals):
312+
313+
def create_observer(j, s, prop, e):
314+
def _cond_observer(_):
315+
# Check whether our data matches the given schema
316+
try:
317+
jsonschema.validate(instance=_getter(), schema=s)
318+
elements[prop] = e
319+
widget_list[len(keys) + j].children = e.widgets
320+
except jsonschema.ValidationError:
321+
widget_list[len(keys) + j].children = []
322+
323+
# We need to call the observer once so that we get a correctly
324+
# initialized widget, because otherwise it triggers only if it
325+
# differs from the default.
326+
_cond_observer({})
327+
328+
return _cond_observer
329+
330+
for k in cschema.get("properties", {}).keys():
331+
elements[k].register_observer(
332+
create_observer(i, cschema, cprop, celem), "value", "change"
333+
)
334+
335+
# Ensure that defaults are initialized
329336
_resetter()
330337

331338
return self.construct_element(
332339
getter=_getter,
333340
setter=_setter,
334341
resetter=_resetter,
335-
widgets=widget_list,
342+
widgets=wrapped_widget_list,
336343
subelements=elements,
337344
register_observer=_register_observer,
338345
)
@@ -372,7 +379,7 @@ def _construct_simple(self, schema, widget, label=None, root=False):
372379
# Apply regex pattern matching
373380
def pattern_checker(val):
374381
# This only makes sense for strings
375-
if schema["type"] != "string":
382+
if schema["type"] != "string" or val is None:
376383
return True
377384

378385
# Try matching the given data against the pattern

tests/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
# Read the test data from the schemas subdirectory
77
_test_data = []
8+
_test_names = []
89
for filename in glob.glob(
910
os.path.join(os.path.split(__file__)[0], "schemas", "*.json")
1011
):
1112
with open(filename, "r") as f:
1213
_test_data.append(json.load(f))
14+
_test_names.append(os.path.basename(filename))
1315

1416

1517
def pytest_generate_tests(metafunc):
1618
if "testcase" in metafunc.fixturenames:
17-
metafunc.parametrize("testcase", _test_data)
19+
metafunc.parametrize("testcase", _test_data, ids=_test_names)

tests/schemas/if-then-same.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"schema": {
3+
"default": {
4+
"type": "integer",
5+
"value": 42
6+
},
7+
"else": {
8+
"if": {
9+
"properties": {
10+
"type": {
11+
"const": "string"
12+
}
13+
}
14+
},
15+
"then": {
16+
"properties": {
17+
"value": {
18+
"type": "string"
19+
}
20+
}
21+
}
22+
},
23+
"if": {
24+
"properties": {
25+
"type": {
26+
"const": "integer"
27+
}
28+
}
29+
},
30+
"properties": {
31+
"type": {
32+
"enum": [
33+
"integer",
34+
"string"
35+
],
36+
"type": "string"
37+
}
38+
},
39+
"then": {
40+
"properties": {
41+
"value": {
42+
"type": "integer"
43+
}
44+
}
45+
},
46+
"type": "object"
47+
},
48+
"valid": [
49+
{
50+
"type": "integer",
51+
"value": 42
52+
},
53+
{
54+
"type": "string",
55+
"value": "foo"
56+
}
57+
]
58+
}

0 commit comments

Comments
 (0)