Skip to content

Commit 00edc81

Browse files
Merge pull request #2193 from reggied/jsonspec
Issue #1979: Add generation of JSON for widget specs which is then used to create MD.
2 parents 835c218 + 5cb9ee1 commit 00edc81

File tree

5 files changed

+6010
-87
lines changed

5 files changed

+6010
-87
lines changed

.github/workflows/testspec.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ jobs:
2323
pip install file://$PWD#egg=ipywidgets
2424
- name: Compare spec with latest version
2525
run: |
26-
python ./packages/schema/generate-spec.py > spec.md
26+
python ./packages/schema/generate-spec.py -f markdown > spec.md
2727
diff -u ./packages/schema/jupyterwidgetmodels.latest.md ./spec.md

docs/source/dev_install.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ Updating widget model specification
7575
7676
To update the widget model specification with changes, do something like this in the repo root:
7777
```
78-
python ./packages/schema/generate-spec.py > packages/schema/jupyterwidgetmodels.latest.md
78+
python ./packages/schema/generate-spec.py -f json-pretty > packages/schema/jupyterwidgetmodels.latest.json
79+
python ./packages/schema/generate-spec.py -f markdown > packages/schema/jupyterwidgetmodels.latest.md
7980
```
8081
8182
Releasing new versions

packages/schema/generate-spec.py

+183-82
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,211 @@
11
# Copyright (c) Jupyter Development Team.
22
# Distributed under the terms of the Modified BSD License.
33

4-
import ipywidgets as widgets
5-
from ipywidgets.widgets.widget_link import Link
4+
import argparse
5+
import json
6+
from operator import itemgetter
67

7-
from traitlets import CaselessStrEnum, Unicode, Tuple, List, Bool, CFloat, Float, CInt, Int, Instance, Dict, Any
8+
from traitlets import (CaselessStrEnum, Unicode, Tuple, List, Bool, CFloat,
9+
Float, CInt, Int, Instance, Dict, Bytes, Any)
10+
11+
import ipywidgets as widgets
812
from ipywidgets import Color
913
from ipywidgets.widgets.trait_types import TypedTuple
14+
from ipywidgets.widgets.widget_link import Link
1015

11-
header = '''# Model State
16+
HEADER = '''# Model State
1217
13-
This is a description of the model state for each widget in the core Jupyter widgets library. The model ID of a widget is the id of the comm object the widget is using. A reference to a widget is serialized to JSON as a string of the form `"IPY_MODEL_<MODEL_ID>"`, where `<MODEL_ID>` is the model ID of a previously created widget of the specified type.
18+
This is a description of the model state for each widget in the core Jupyter
19+
widgets library. The model ID of a widget is the id of the comm object the
20+
widget is using. A reference to a widget is serialized to JSON as a string of
21+
the form `"IPY_MODEL_<MODEL_ID>"`, where `<MODEL_ID>` is the model ID of a
22+
previously created widget of the specified type.
1423
15-
This model specification is for ipywidgets 7.4.*, @jupyter-widgets/base 1.1.*, and @jupyter-widgets/controls 1.4.*.
24+
This model specification is for ipywidgets 7.4.*, @jupyter-widgets/base 1.1.*,
25+
and @jupyter-widgets/controls 1.4.*.
1626
1727
## Model attributes
1828
19-
Each widget in the Jupyter core widgets is represented below. The heading represents the model name, module, and version, view name, module, and version that the widget is registered with.
29+
Each widget in the Jupyter core widgets is represented below. The heading
30+
represents the model name, module, and version, view name, module, and version
31+
that the widget is registered with.
2032
2133
'''
2234

23-
widgets_to_document = sorted(widgets.Widget.widget_types.items())
24-
25-
def typing(x):
26-
s = ''
27-
if isinstance(x, CaselessStrEnum):
28-
s = 'string (one of %s)'%(', '.join('`%r`'%i for i in x.values))
29-
elif isinstance(x, Unicode):
30-
s = 'string'
31-
elif isinstance(x, (Tuple, List)):
32-
s = 'array'
33-
elif isinstance(x, TypedTuple):
34-
s = 'array of ' + typing(x._trait)
35-
elif isinstance(x, Bool):
36-
s = 'boolean'
37-
elif isinstance(x, (CFloat, Float)):
38-
s = 'number (float)'
39-
elif isinstance(x, (CInt, Int)):
40-
s = 'number (integer)'
41-
elif isinstance(x, Color):
42-
s = 'string (valid color)'
43-
elif isinstance(x, Dict):
44-
s = 'object'
45-
elif isinstance(x, Instance) and issubclass(x.klass, widgets.Widget):
46-
s = 'reference to %s widget'%(x.klass.__name__)
35+
NUMBER_MAP = {
36+
'int': 'number (integer)',
37+
'float': 'number (float)',
38+
'bool': 'boolean',
39+
'bytes': 'Bytes'
40+
}
41+
42+
43+
def widget_type(widget, widget_list):
44+
attributes = {}
45+
if isinstance(widget, CaselessStrEnum):
46+
w_type = 'string'
47+
attributes['enum'] = widget.values
48+
elif isinstance(widget, Unicode):
49+
w_type = 'string'
50+
elif isinstance(widget, (Tuple, List)):
51+
w_type = 'array'
52+
elif isinstance(widget, TypedTuple):
53+
w_type = 'array'
54+
attributes['items'] = widget_type(widget._trait, widget_list)
55+
elif isinstance(widget, Bool):
56+
w_type = 'bool'
57+
elif isinstance(widget, (CFloat, Float)):
58+
w_type = 'float'
59+
elif isinstance(widget, (CInt, Int)):
60+
w_type = 'int'
61+
elif isinstance(widget, Color):
62+
w_type = 'color'
63+
elif isinstance(widget, Dict):
64+
w_type = 'object'
65+
elif isinstance(widget, Bytes):
66+
w_type = 'bytes'
67+
elif isinstance(widget, Instance) and issubclass(widget.klass,
68+
widgets.Widget):
69+
w_type = 'reference'
70+
attributes['widget'] = widget.klass.__name__
4771
# ADD the widget to this documenting list
48-
if x.klass not in [i[1] for i in widgets_to_document] and x.klass != widgets.Widget:
49-
widgets_to_document.append((x.klass.__name__, x.klass))
50-
elif isinstance(x, Any):
51-
# In our case, these all happen to be values that are converted to strings
52-
s = 'string (valid option label)'
72+
if (widget.klass not in [i[1] for i in widget_list]
73+
and widget.klass is not widgets.Widget):
74+
widget_list.append((widget.klass.__name__, widget.klass))
75+
elif isinstance(widget, Any):
76+
# In our case, these all happen to be values that are converted to
77+
# strings
78+
w_type = 'label'
5379
else:
54-
s = x.__class__.__name__
55-
if x.allow_none:
56-
s = "`null` or "+s
57-
return s
58-
59-
def jsdefault(t):
60-
x = t.default_value
61-
if isinstance(t, Instance):
62-
x = t.make_dynamic_default()
63-
if issubclass(t.klass, widgets.Widget):
80+
w_type = widget.__class__.__name__
81+
attributes['type'] = w_type
82+
if widget.allow_none:
83+
attributes['allow_none'] = True
84+
return attributes
85+
86+
87+
def jsdefault(trait):
88+
if isinstance(trait, Instance):
89+
default = trait.make_dynamic_default()
90+
if issubclass(trait.klass, widgets.Widget):
6491
return 'reference to new instance'
65-
if x is True:
66-
return '`true`'
67-
elif x is False:
68-
return '`false`'
69-
elif x is None:
70-
return '`null`'
71-
elif isinstance(x, tuple):
72-
return '`{}`'.format(list(x))
7392
else:
74-
return '`%s`'%t.default_value_repr()
93+
default = trait.default_value
94+
if isinstance(default, bytes):
95+
default = trait.default_value_repr()
96+
return default
97+
98+
99+
def mddefault(attribute):
100+
default = attribute['default']
101+
is_ref = isinstance(default, str) and default.startswith('reference')
102+
if default is None:
103+
default = 'null'
104+
elif isinstance(default, bool):
105+
default = str(default).lower()
106+
elif not is_ref and attribute['type'] != 'bytes':
107+
default = "{!r}".format(default)
108+
if not is_ref:
109+
default = '`{}`'.format(default)
110+
return default
75111

76-
def format_widget(n, w):
77-
out = []
78-
name = dict(zip(['m_module', 'm_version', 'model', 'v_module', 'v_version', 'view'], n))
79112

80-
out.append('### %(model)s (%(m_module)s, %(m_version)s); %(view)s (%(v_module)s, %(v_version)s)'%name)
113+
def mdtype(attribute):
114+
md_type = attribute['type']
115+
if md_type in NUMBER_MAP:
116+
md_type = NUMBER_MAP[md_type]
117+
if attribute.get('allow_none'):
118+
md_type = '`null` or {}'.format(md_type)
119+
if 'enum' in attribute:
120+
md_type = '{} (one of {})'.format(
121+
md_type, ', '.join('`{!r}`'.format(n) for n in attribute['enum'])
122+
)
123+
if 'items' in attribute:
124+
md_type = '{} of {}'.format(md_type, mdtype(attribute['items']))
125+
if 'widget' in attribute:
126+
md_type = '{} to {} widget'.format(md_type, attribute['widget'])
127+
return md_type
128+
129+
130+
def format_widget(widget):
131+
out = []
132+
fmt = '%(name)s (%(module)s, %(version)s)'
133+
out.append('### %s; %s' % (fmt % widget['model'], fmt % widget['view']))
81134
out.append('')
82-
out.append('{name: <16} | {typing: <16} | {default: <16} | {help}'.format(name='Attribute', typing='Type',
83-
default='Default', help='Help'))
135+
out.append('{name: <16} | {typing: <16} | {default: <16} | {help}'.format(
136+
name='Attribute', typing='Type', default='Default', help='Help')
137+
)
84138
out.append('{0:-<16}-|-{0:-<16}-|-{0:-<16}-|----'.format('-'))
85-
for name, t in sorted(w.traits(sync=True).items()):
86-
if name in ('_model_module', '_view_module', '_model_module_version', '_view_module_version',
87-
'_dom_classes', 'layout'):
88-
# document these separately, since they apply to all classes
89-
pass
90-
if name in ('_view_count'):
91-
# don't document this since it is totally experimental at this point
92-
continue
93139

94-
s = '{name: <16} | {typing: <16} | {default: <16} | {help}'.format(name='`%s`'%name, typing=typing(t),
95-
allownone='*' if t.allow_none else '',
96-
default=jsdefault(t),
97-
help=t.help if t.help else '')
140+
for attribute in sorted(widget['attributes'], key=itemgetter('name')):
141+
s = '{name: <16} | {type: <16} | {default: <16} | {help}'.format(
142+
name='`{}`'.format(attribute['name']),
143+
default=mddefault(attribute),
144+
type=mdtype(attribute),
145+
help=attribute['help']
146+
)
98147
out.append(s)
99148
out.append('')
100149
return '\n'.join(out)
101150

102-
out = header
103-
for n,w in widgets_to_document:
104-
if issubclass(w, Link):
105-
out += '\n'+format_widget(n, w((widgets.IntSlider(), 'value'), (widgets.IntSlider(), 'value')))
106-
elif issubclass(w, widgets.SelectionRangeSlider) or issubclass(w, widgets.SelectionSlider):
107-
out += '\n'+format_widget(n,w(options=[1]))
108-
else:
109-
out += '\n'+format_widget(n,w())
110-
print(out)
151+
152+
def jsonify(identifier, widget, widget_list):
153+
model = dict(zip(['module', 'version', 'name'], identifier[:3]))
154+
view = dict(zip(['module', 'version', 'name'], identifier[3:]))
155+
attributes = []
156+
for name, trait in widget.traits(sync=True).items():
157+
if name == '_view_count':
158+
# don't document this since it is totally experimental at this point
159+
continue
160+
161+
attribute = dict(
162+
name=name,
163+
help=trait.help or '',
164+
default=jsdefault(trait)
165+
)
166+
attribute.update(widget_type(trait, widget_list))
167+
attributes.append(attribute)
168+
169+
return dict(model=model, view=view, attributes=attributes)
170+
171+
172+
def create_spec(widget_list):
173+
widget_data = []
174+
for widget_name, widget_cls in widget_list:
175+
if issubclass(widget_cls, Link):
176+
widget = widget_cls((widgets.IntSlider(), 'value'),
177+
(widgets.IntSlider(), 'value'))
178+
elif issubclass(widget_cls, (widgets.SelectionRangeSlider,
179+
widgets.SelectionSlider)):
180+
widget = widget_cls(options=[1])
181+
else:
182+
widget = widget_cls()
183+
184+
widget_data.append(jsonify(widget_name, widget, widget_list))
185+
return widget_data
186+
187+
188+
def create_markdown(spec):
189+
output = [HEADER]
190+
for widget in spec:
191+
output.append(format_widget(widget))
192+
return '\n'.join(output)
193+
194+
195+
if __name__ == '__main__':
196+
parser = argparse.ArgumentParser(description='Description of your program')
197+
parser.add_argument('-f', '--format', choices=['json', 'json-pretty', 'markdown'],
198+
help='Format to generate', default='json')
199+
args = parser.parse_args()
200+
format = args.format
201+
202+
widgets_to_document = sorted(widgets.Widget.widget_types.items())
203+
spec = create_spec(widgets_to_document)
204+
if format == 'json':
205+
print(json.dumps(spec, sort_keys=True))
206+
elif format == 'json-pretty':
207+
print(json.dumps(spec, sort_keys=True,
208+
indent=2, separators=(',', ': ')))
209+
elif format == 'markdown':
210+
# We go through the json engine to convert tuples to lists, etc.
211+
print(create_markdown(json.loads(json.dumps(spec))))

0 commit comments

Comments
 (0)