diff --git a/dargs/notebook.py b/dargs/notebook.py new file mode 100644 index 0000000..30b9708 --- /dev/null +++ b/dargs/notebook.py @@ -0,0 +1,307 @@ +r'''IPython/Jupyter Notebook display for dargs. + +It is expected to be used in Jupyter Notebook, where +the IPython module is available. + +Examples +-------- +>>> from dargs.sphinx import _test_argument +>>> from dargs.notebook import JSON +>>> jstr = """ +... { +... "test_argument": "test1", +... "test_variant": "test_variant_argument", +... "_comment": "This is an example data" +... } +... """ +>>> JSON(jstr, _test_argument()) +''' + +import json +import re +from typing import List, Union + +from IPython.display import HTML, display + +from dargs import Argument, Variant + +__all__ = ["JSON"] + +# https://www.w3schools.com/css/css_tooltip.asp +css = """ +""" + + +def JSON(data: Union[dict, str], arg: Union[Argument, List[Argument]]): + """Display JSON data with Argument in the Jupyter Notebook. + + Parameters + ---------- + data : dict or str + The JSON data to be displayed, either JSON string or a dict. + arg : dargs.Argument or list[dargs.Argument] + The Argument that describes the JSON data. + """ + display(HTML(print_html(data, arg))) + + +def print_html(data: Union[dict, str], arg: Union[Argument, List[Argument]]) -> str: + """Print HTML string with Argument in the Jupyter Notebook. + + Parameters + ---------- + data : dict or str + The JSON data to be displayed, either JSON string or a dict. + arg : dargs.Argument or list[dargs.Argument] + The Argument that describes the JSON data. + + Returns + ------- + str + The HTML string. + """ + if isinstance(data, str): + data = json.loads(data) + elif isinstance(data, dict): + pass + else: + raise ValueError(f"Unknown type: {type(data)}") + + if isinstance(arg, list): + arg = Argument("data", dtype=dict, sub_fields=arg) + elif isinstance(arg, Argument): + pass + else: + raise ValueError(f"Unknown type: {type(arg)}") + argdata = ArgumentData(data, arg) + buff = [css, r"""
""", argdata.print_html(), r"
"] + return "".join(buff) + + +class ArgumentData: + """ArgumentData is a class to hold the data and Argument. + + It is used to print the data with Argument in the Jupyter Notebook. + + Parameters + ---------- + data : dict + The data to be displayed. + arg : dargs.Argument + The Argument that describes the data. + """ + + def __init__(self, data: dict, arg: Argument): + self.data = data + self.arg = arg + self.subdata = [] + self._init_subdata() + + def _init_subdata(self): + """Initialize sub ArgumentData.""" + if isinstance(self.data, dict) and isinstance(self.arg, Argument): + sub_fields = self.arg.sub_fields.copy() + # extend subfiles with sub_variants + for vv in self.arg.sub_variants.values(): + choice = self.data.get(vv.flag_name, vv.default_tag) + if choice and choice in vv.choice_dict: + sub_fields.update(vv.choice_dict[choice].sub_fields) + + for kk in self.data: + if kk in sub_fields: + self.subdata.append(ArgumentData(self.data[kk], sub_fields[kk])) + elif kk in self.arg.sub_variants: + self.subdata.append( + ArgumentData(self.data[kk], self.arg.sub_variants[kk]) + ) + else: + self.subdata.append(ArgumentData(self.data[kk], kk)) + elif ( + isinstance(self.data, list) + and isinstance(self.arg, Argument) + and self.arg.repeat + ): + for dd in self.data: + self.subdata.append(ArgumentData(dd, self.arg)) + + def print_html(self, _level=0, _last_one=True): + """Print the data with Argument in HTML format. + + Parameters + ---------- + _level : int, optional + The level of indentation, by default 0 + _last_one : bool, optional + Whether it is the last one, by default True + """ + linebreak = "
" + indent = ( + r"""""" + + " " * (_level * 2) + + "" + ) + buff = [] + buff.append(indent) + if _level > 0 and not ( + isinstance(self.data, dict) + and isinstance(self.arg, Argument) + and self.arg.repeat + ): + if isinstance(self.arg, (Argument, Variant)): + buff.append(r"""""") + else: + buff.append(r"""""") + buff.append(r"""""") + buff.append('"') + if isinstance(self.arg, Argument): + buff.append(self.arg.name) + elif isinstance(self.arg, Variant): + buff.append(self.arg.flag_name) + elif isinstance(self.arg, str): + buff.append(self.arg) + else: + raise ValueError(f"Unknown type: {type(self.arg)}") + buff.append('"') + buff.append("") + if isinstance(self.arg, (Argument, Variant)): + buff.append(r"""""") + if isinstance(self.arg, Argument): + doc_head = ( + self.arg.gen_doc_head() + .replace("| type:", "type:") + .replace("\n", linebreak) + ) + # use re to replace ``xx`` to xx + doc_head = re.sub( + r"``(.*?)``", + r'\1', + doc_head, + ) + doc_head = re.sub(r"\*(.+)\*", r"\1", doc_head) + buff.append(doc_head) + elif isinstance(self.arg, Variant): + buff.append(f"{self.arg.flag_name}:
type: ") + buff.append(r"""""") + buff.append("str") + buff.append(r"""""") + if self.arg.default_tag: + buff.append(", default: ") + buff.append(r"""""") + buff.append(self.arg.default_tag) + buff.append(r"""""") + else: + raise ValueError(f"Unknown type: {type(self.arg)}") + + doc_body = self.arg.doc.strip() + if doc_body: + buff.append("
") + doc_body = re.sub(r"""\n+""", "\n", doc_body) + doc_body = doc_body.replace("\n", linebreak) + doc_body = re.sub( + r"`+(.*?)`+", r'\1', doc_body + ) + doc_body = re.sub(r"\*(.+)\*", r"\1", doc_body) + buff.append(doc_body) + + buff.append(r"""
""") + buff.append(r"""
""") + buff.append(r"""""") + buff.append(": ") + buff.append("") + if self.subdata and isinstance(self.data, dict): + buff.append(r"""""") + buff.append("{") + buff.append("") + buff.append(linebreak) + for ii, sub in enumerate(self.subdata): + buff.append( + sub.print_html(_level + 1, _last_one=(ii == len(self.subdata) - 1)) + ) + buff.append(indent) + buff.append(r"""""") + buff.append("}") + if not _last_one: + buff.append(",") + buff.append("") + buff.append(linebreak) + elif self.subdata and isinstance(self.data, list): + buff.append(r"""""") + buff.append("[") + buff.append("") + buff.append(linebreak) + for ii, sub in enumerate(self.subdata): + buff.append( + sub.print_html(_level + 1, _last_one=(ii == len(self.subdata) - 1)) + ) + buff.append(indent) + buff.append(r"""""") + buff.append("]") + if not _last_one: + buff.append(",") + buff.append("") + buff.append(linebreak) + else: + buff.append(r"""""") + buff.append( + json.dumps(self.data, indent=2).replace("\n", f"{linebreak}{indent}") + ) + if not _last_one: + buff.append(",") + buff.append("") + buff.append(linebreak) + return "".join(buff) diff --git a/dargs/sphinx.py b/dargs/sphinx.py index 2c7aa82..986f743 100644 --- a/dargs/sphinx.py +++ b/dargs/sphinx.py @@ -176,7 +176,25 @@ def _test_argument() -> Argument: "test_variant", doc=doc_test, choices=[ - Argument("test_variant_argument", dtype=str, doc=doc_test), + Argument( + "test_variant_argument", + dtype=dict, + optional=True, + doc=doc_test, + sub_fields=[ + Argument( + "test_repeat", + dtype=list, + repeat=True, + doc=doc_test, + sub_fields=[ + Argument( + "test_repeat_item", dtype=bool, doc=doc_test + ), + ], + ) + ], + ), ], ), ], diff --git a/docs/conf.py b/docs/conf.py index 4c106fc..88c1a40 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,7 +45,7 @@ "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "numpydoc", - "myst_parser", + "myst_nb", "dargs.sphinx", ] diff --git a/docs/index.rst b/docs/index.rst index 482500d..5d6f0f2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Welcome to dargs's documentation! intro sphinx dpgui + nb api/api credits diff --git a/docs/nb.ipynb b/docs/nb.ipynb new file mode 100644 index 0000000..1793515 --- /dev/null +++ b/docs/nb.ipynb @@ -0,0 +1,50 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Use with Jupyter Notebook\n", + "\n", + "In a [Jupyter Notebook](https://jupyter.org/), with {meth}`dargs.notebook.JSON`, one can render an JSON string with an Argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dargs.sphinx import _test_argument\n", + "from dargs.notebook import JSON\n", + "\n", + "jstr = \"\"\"\n", + "{\n", + " \"test_argument\": \"test1\",\n", + " \"test_variant\": \"test_variant_argument\",\n", + " \"test_repeat\": [\n", + " {\"test_repeat_item\": false},\n", + " {\"test_repeat_item\": true}\n", + " ],\n", + " \"_comment\": \"This is an example data\"\n", + "}\n", + "\"\"\"\n", + "JSON(jstr, _test_argument())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When the monse stays on an argument key, the documentation of this argument will pop up." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/requirements.txt b/docs/requirements.txt index 54fdba7..d2c7bc5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ . numpydoc deepmodeling_sphinx>=0.1.1 -myst_parser +myst-nb sphinx_rtd_theme diff --git a/pyproject.toml b/pyproject.toml index ab89bf1..0a6b0fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,11 @@ Homepage = "https://github.com/deepmodeling/dargs" documentation = "https://docs.deepmodeling.com/projects/dargs" repository = "https://github.com/deepmodeling/dargs" +[project.optional-dependencies] +test = [ + "ipython", +] + [tool.setuptools.packages.find] include = ["dargs*"] diff --git a/tests/test_notebook.py b/tests/test_notebook.py new file mode 100644 index 0000000..01e46a7 --- /dev/null +++ b/tests/test_notebook.py @@ -0,0 +1,70 @@ +import unittest +from xml.etree import ElementTree as ET + +from dargs import Argument, Variant + +try: + import IPython # noqa: F401 +except ImportError: + ipython_installed = False +else: + ipython_installed = True + + +@unittest.skipUnless(ipython_installed, "IPython not installed") +class TestNotebook(unittest.TestCase): + def test_html_validation(self): + from dargs.notebook import print_html + + doc_test = "Test doc." + test_arg = Argument( + name="test", + dtype=str, + doc=doc_test, + sub_fields=[ + Argument("test_argument", dtype=str, doc=doc_test, default="test"), + ], + sub_variants=[ + Variant( + "test_variant", + doc=doc_test, + choices=[ + Argument( + "test_variant_argument", + dtype=dict, + optional=True, + doc=doc_test, + sub_fields=[ + Argument( + "test_repeat", + dtype=list, + repeat=True, + doc=doc_test, + sub_fields=[ + Argument( + "test_repeat_item", dtype=bool, doc=doc_test + ), + ], + ) + ], + ), + ], + ), + ], + ) + jdata = { + "test_argument": "test1", + "test_variant": "test_variant_argument", + "test_repeat": [{"test_repeat_item": False}, {"test_repeat_item": True}], + "_comment": "This is an example data", + } + html = print_html( + jdata, + test_arg, + ) + # https://stackoverflow.com/a/29533744/9567349 + # https://stackoverflow.com/a/35591479/9567349 + magic = """ + ]>""" + ET.fromstring(magic + f"{html}")