Skip to content

Commit 1e866c9

Browse files
Support rendering JSON in a Jupyter Notebook (#44)
Signed-off-by: Jinzhe Zeng <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 23edb17 commit 1e866c9

File tree

8 files changed

+454
-3
lines changed

8 files changed

+454
-3
lines changed

dargs/notebook.py

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
r'''IPython/Jupyter Notebook display for dargs.
2+
3+
It is expected to be used in Jupyter Notebook, where
4+
the IPython module is available.
5+
6+
Examples
7+
--------
8+
>>> from dargs.sphinx import _test_argument
9+
>>> from dargs.notebook import JSON
10+
>>> jstr = """
11+
... {
12+
... "test_argument": "test1",
13+
... "test_variant": "test_variant_argument",
14+
... "_comment": "This is an example data"
15+
... }
16+
... """
17+
>>> JSON(jstr, _test_argument())
18+
'''
19+
20+
import json
21+
import re
22+
from typing import List, Union
23+
24+
from IPython.display import HTML, display
25+
26+
from dargs import Argument, Variant
27+
28+
__all__ = ["JSON"]
29+
30+
# https://www.w3schools.com/css/css_tooltip.asp
31+
css = """<style>
32+
.dargs-codeblock {
33+
width: 100%;
34+
background-color: #f9f9f9;
35+
border-color: #f9f9f9;
36+
color: #000000;
37+
}
38+
.dargs-codeblock::before {
39+
counter-reset: listing;
40+
}
41+
.dargs-codeblock code.dargs-linebegin {
42+
counter-increment: listing;
43+
}
44+
.dargs-codeblock code.dargs-linebegin::before {
45+
content: counter(listing) " ";
46+
display: inline-block;
47+
width: 2em;
48+
padding-left: auto;
49+
margin-left: auto;
50+
text-align: right;
51+
color: #6e7781;
52+
}
53+
.dargs-codeblock code.dargs-code {
54+
padding-left: 0;
55+
padding-right: 0;
56+
margin-left: 0;
57+
margin-right: 0;
58+
background-color: #f9f9f9;
59+
border-color: #f9f9f9;
60+
color: #000000;
61+
}
62+
.dargs-codeblock .dargs-key {
63+
position: relative;
64+
display: inline-block;
65+
border-bottom: 1px dotted black;
66+
}
67+
.dargs-codeblock .dargs-key code.dargs-code {
68+
color: #0550ae;
69+
}
70+
.dargs-codeblock .dargs-key .dargs-doc {
71+
visibility: hidden;
72+
width: 600px;
73+
background-color: black;
74+
color: #fff;
75+
padding: 1em 1em;
76+
border-radius: 6px;
77+
position: absolute;
78+
z-index: 1;
79+
}
80+
.dargs-codeblock .dargs-key:hover .dargs-doc {
81+
visibility: visible;
82+
}
83+
.dargs-codeblock .dargs-key .dargs-doc .dargs-doc-code {
84+
color: #bbbbff;
85+
}
86+
</style>
87+
"""
88+
89+
90+
def JSON(data: Union[dict, str], arg: Union[Argument, List[Argument]]):
91+
"""Display JSON data with Argument in the Jupyter Notebook.
92+
93+
Parameters
94+
----------
95+
data : dict or str
96+
The JSON data to be displayed, either JSON string or a dict.
97+
arg : dargs.Argument or list[dargs.Argument]
98+
The Argument that describes the JSON data.
99+
"""
100+
display(HTML(print_html(data, arg)))
101+
102+
103+
def print_html(data: Union[dict, str], arg: Union[Argument, List[Argument]]) -> str:
104+
"""Print HTML string with Argument in the Jupyter Notebook.
105+
106+
Parameters
107+
----------
108+
data : dict or str
109+
The JSON data to be displayed, either JSON string or a dict.
110+
arg : dargs.Argument or list[dargs.Argument]
111+
The Argument that describes the JSON data.
112+
113+
Returns
114+
-------
115+
str
116+
The HTML string.
117+
"""
118+
if isinstance(data, str):
119+
data = json.loads(data)
120+
elif isinstance(data, dict):
121+
pass
122+
else:
123+
raise ValueError(f"Unknown type: {type(data)}")
124+
125+
if isinstance(arg, list):
126+
arg = Argument("data", dtype=dict, sub_fields=arg)
127+
elif isinstance(arg, Argument):
128+
pass
129+
else:
130+
raise ValueError(f"Unknown type: {type(arg)}")
131+
argdata = ArgumentData(data, arg)
132+
buff = [css, r"""<div class="dargs-codeblock">""", argdata.print_html(), r"</div>"]
133+
return "".join(buff)
134+
135+
136+
class ArgumentData:
137+
"""ArgumentData is a class to hold the data and Argument.
138+
139+
It is used to print the data with Argument in the Jupyter Notebook.
140+
141+
Parameters
142+
----------
143+
data : dict
144+
The data to be displayed.
145+
arg : dargs.Argument
146+
The Argument that describes the data.
147+
"""
148+
149+
def __init__(self, data: dict, arg: Argument):
150+
self.data = data
151+
self.arg = arg
152+
self.subdata = []
153+
self._init_subdata()
154+
155+
def _init_subdata(self):
156+
"""Initialize sub ArgumentData."""
157+
if isinstance(self.data, dict) and isinstance(self.arg, Argument):
158+
sub_fields = self.arg.sub_fields.copy()
159+
# extend subfiles with sub_variants
160+
for vv in self.arg.sub_variants.values():
161+
choice = self.data.get(vv.flag_name, vv.default_tag)
162+
if choice and choice in vv.choice_dict:
163+
sub_fields.update(vv.choice_dict[choice].sub_fields)
164+
165+
for kk in self.data:
166+
if kk in sub_fields:
167+
self.subdata.append(ArgumentData(self.data[kk], sub_fields[kk]))
168+
elif kk in self.arg.sub_variants:
169+
self.subdata.append(
170+
ArgumentData(self.data[kk], self.arg.sub_variants[kk])
171+
)
172+
else:
173+
self.subdata.append(ArgumentData(self.data[kk], kk))
174+
elif (
175+
isinstance(self.data, list)
176+
and isinstance(self.arg, Argument)
177+
and self.arg.repeat
178+
):
179+
for dd in self.data:
180+
self.subdata.append(ArgumentData(dd, self.arg))
181+
182+
def print_html(self, _level=0, _last_one=True):
183+
"""Print the data with Argument in HTML format.
184+
185+
Parameters
186+
----------
187+
_level : int, optional
188+
The level of indentation, by default 0
189+
_last_one : bool, optional
190+
Whether it is the last one, by default True
191+
"""
192+
linebreak = "<br/>"
193+
indent = (
194+
r"""<code class="dargs-code dargs-linebegin">"""
195+
+ "&nbsp;" * (_level * 2)
196+
+ "</code>"
197+
)
198+
buff = []
199+
buff.append(indent)
200+
if _level > 0 and not (
201+
isinstance(self.data, dict)
202+
and isinstance(self.arg, Argument)
203+
and self.arg.repeat
204+
):
205+
if isinstance(self.arg, (Argument, Variant)):
206+
buff.append(r"""<span class="dargs-key">""")
207+
else:
208+
buff.append(r"""<span>""")
209+
buff.append(r"""<code class="dargs-code">""")
210+
buff.append('"')
211+
if isinstance(self.arg, Argument):
212+
buff.append(self.arg.name)
213+
elif isinstance(self.arg, Variant):
214+
buff.append(self.arg.flag_name)
215+
elif isinstance(self.arg, str):
216+
buff.append(self.arg)
217+
else:
218+
raise ValueError(f"Unknown type: {type(self.arg)}")
219+
buff.append('"')
220+
buff.append("</code>")
221+
if isinstance(self.arg, (Argument, Variant)):
222+
buff.append(r"""<span class="dargs-doc">""")
223+
if isinstance(self.arg, Argument):
224+
doc_head = (
225+
self.arg.gen_doc_head()
226+
.replace("| type:", "type:")
227+
.replace("\n", linebreak)
228+
)
229+
# use re to replace ``xx`` to <code>xx</code>
230+
doc_head = re.sub(
231+
r"``(.*?)``",
232+
r'<span class="dargs-doc-code">\1</span>',
233+
doc_head,
234+
)
235+
doc_head = re.sub(r"\*(.+)\*", r"<i>\1</i>", doc_head)
236+
buff.append(doc_head)
237+
elif isinstance(self.arg, Variant):
238+
buff.append(f"{self.arg.flag_name}:<br/>type: ")
239+
buff.append(r"""<span class="dargs-doc-code">""")
240+
buff.append("str")
241+
buff.append(r"""</span>""")
242+
if self.arg.default_tag:
243+
buff.append(", default: ")
244+
buff.append(r"""<span class="dargs-doc-code">""")
245+
buff.append(self.arg.default_tag)
246+
buff.append(r"""</span>""")
247+
else:
248+
raise ValueError(f"Unknown type: {type(self.arg)}")
249+
250+
doc_body = self.arg.doc.strip()
251+
if doc_body:
252+
buff.append("<hr/>")
253+
doc_body = re.sub(r"""\n+""", "\n", doc_body)
254+
doc_body = doc_body.replace("\n", linebreak)
255+
doc_body = re.sub(
256+
r"`+(.*?)`+", r'<span class="dargs-doc-code">\1</span>', doc_body
257+
)
258+
doc_body = re.sub(r"\*(.+)\*", r"<i>\1</i>", doc_body)
259+
buff.append(doc_body)
260+
261+
buff.append(r"""</span>""")
262+
buff.append(r"""</span>""")
263+
buff.append(r"""<code class="dargs-code">""")
264+
buff.append(": ")
265+
buff.append("</code>")
266+
if self.subdata and isinstance(self.data, dict):
267+
buff.append(r"""<code class="dargs-code">""")
268+
buff.append("{")
269+
buff.append("</code>")
270+
buff.append(linebreak)
271+
for ii, sub in enumerate(self.subdata):
272+
buff.append(
273+
sub.print_html(_level + 1, _last_one=(ii == len(self.subdata) - 1))
274+
)
275+
buff.append(indent)
276+
buff.append(r"""<code class="dargs-code">""")
277+
buff.append("}")
278+
if not _last_one:
279+
buff.append(",")
280+
buff.append("</code>")
281+
buff.append(linebreak)
282+
elif self.subdata and isinstance(self.data, list):
283+
buff.append(r"""<code class="dargs-code">""")
284+
buff.append("[")
285+
buff.append("</code>")
286+
buff.append(linebreak)
287+
for ii, sub in enumerate(self.subdata):
288+
buff.append(
289+
sub.print_html(_level + 1, _last_one=(ii == len(self.subdata) - 1))
290+
)
291+
buff.append(indent)
292+
buff.append(r"""<code class="dargs-code">""")
293+
buff.append("]")
294+
if not _last_one:
295+
buff.append(",")
296+
buff.append("</code>")
297+
buff.append(linebreak)
298+
else:
299+
buff.append(r"""<code class="dargs-code">""")
300+
buff.append(
301+
json.dumps(self.data, indent=2).replace("\n", f"{linebreak}{indent}")
302+
)
303+
if not _last_one:
304+
buff.append(",")
305+
buff.append("</code>")
306+
buff.append(linebreak)
307+
return "".join(buff)

dargs/sphinx.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,25 @@ def _test_argument() -> Argument:
176176
"test_variant",
177177
doc=doc_test,
178178
choices=[
179-
Argument("test_variant_argument", dtype=str, doc=doc_test),
179+
Argument(
180+
"test_variant_argument",
181+
dtype=dict,
182+
optional=True,
183+
doc=doc_test,
184+
sub_fields=[
185+
Argument(
186+
"test_repeat",
187+
dtype=list,
188+
repeat=True,
189+
doc=doc_test,
190+
sub_fields=[
191+
Argument(
192+
"test_repeat_item", dtype=bool, doc=doc_test
193+
),
194+
],
195+
)
196+
],
197+
),
180198
],
181199
),
182200
],

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"sphinx.ext.viewcode",
4646
"sphinx.ext.intersphinx",
4747
"numpydoc",
48-
"myst_parser",
48+
"myst_nb",
4949
"dargs.sphinx",
5050
]
5151

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Welcome to dargs's documentation!
1313
intro
1414
sphinx
1515
dpgui
16+
nb
1617
api/api
1718
credits
1819

0 commit comments

Comments
 (0)