diff --git a/.gitignore b/.gitignore index 41ee080cf..cd48a3e25 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ _version.py __pycache__ docs/_build docs/formats.csv +docs/drivers.csv +docs/minimizers.csv docs/api/ +docs/formats/ diff --git a/docs/formats.rst b/docs/formats.rst index c0ff5b8f9..41ca90916 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -6,3 +6,10 @@ dpdata supports the following formats: .. csv-table:: Supported Formats :file: formats.csv :header-rows: 1 + + +.. toctree:: + :maxdepth: 1 + :glob: + + formats/* diff --git a/docs/make_format.py b/docs/make_format.py index a208b21ed..62dd08793 100644 --- a/docs/make_format.py +++ b/docs/make_format.py @@ -1,9 +1,18 @@ import csv +import os from collections import defaultdict +from inspect import Parameter, Signature, cleandoc, signature +from typing import Literal + +from numpydoc.docscrape import Parameter as numpydoc_Parameter +from numpydoc.docscrape_sphinx import SphinxDocString + +from dpdata.bond_order_system import BondOrderSystem # ensure all plugins are loaded! from dpdata.driver import Driver, Minimizer from dpdata.format import Format +from dpdata.system import LabeledSystem, MultiSystems, System def get_formats() -> dict: @@ -64,7 +73,7 @@ def get_cls_link(cls: object) -> str: def check_supported(fmt: Format): - methods = set() + methods = {} for mtd in [ "from_system", "to_system", @@ -76,33 +85,255 @@ def check_supported(fmt: Format): "to_multi_systems", ]: if detect_overridden(fmt, mtd): - methods.add(mtd) + methods[mtd] = None if mtd == "to_system": - methods.add("to_labeled_system") + methods["to_labeled_system"] = None if fmt.MultiMode != fmt.MultiModes.NotImplemented: - methods.add("from_multi_systems") - methods.add("to_multi_systems") - return methods + methods["from_multi_systems"] = None + methods["to_multi_systems"] = None + return list(methods.keys()) method_links = { - "from_system": ":func:`System() `", - "to_system": ":func:`System.to() `", - "from_labeled_system": ":func:`LabeledSystem() `", - "to_labeled_system": ":func:`LabeledSystem.to() `", - "from_bond_order_system": ":func:`BondOrderSystem() `", - "to_bond_order_system": ":func:`BondOrderSystem.to() `", - "from_multi_systems": ":func:`MultiSystems.load_systems_from_file() `", - "to_multi_systems": ":func:`MultiSystems.to() `", + "from_system": ":ref:`System() <{}_{}>`", + "to_system": ":ref:`System.to() <{}_{}>`", + "from_labeled_system": ":ref:`LabeledSystem() <{}_{}>`", + "to_labeled_system": ":ref:`LabeledSystem.to() <{}_{}>`", + "from_bond_order_system": ":ref:`BondOrderSystem() <{}_{}>`", + "to_bond_order_system": ":ref:`BondOrderSystem.to() <{}_{}>`", + "from_multi_systems": ":ref:`MultiSystems.load_systems_from_file() <{}_{}>`", + "to_multi_systems": ":ref:`MultiSystems.to() <{}_{}>`", +} + +method_classes = { + "from_system": "System", + "to_system": "System", + "from_labeled_system": "LabeledSystem", + "to_labeled_system": "LabeledSystem", + "from_bond_order_system": "BondOrderSystem", + "to_bond_order_system": "BondOrderSystem", + "from_multi_systems": "MultiSystems", + "to_multi_systems": "MultiSystems", } +method_cls_obj = { + "from_system": System, + "to_system": System, + "from_labeled_system": LabeledSystem, + "to_labeled_system": LabeledSystem, + "from_bond_order_system": BondOrderSystem, + "to_bond_order_system": BondOrderSystem, + "from_multi_systems": MultiSystems, + "to_multi_systems": MultiSystems, +} + + +def generate_sub_format_pages(formats: dict): + """Generate sub format pages.""" + os.makedirs("formats", exist_ok=True) + for format, alias in formats.items(): + # format: Format, alias: list[str] + buff = [] + buff.append(".. _%s:" % format.__name__) + buff.append("") + for aa in alias: + buff.append("%s format" % aa) + buff.append("=" * len(buff[-1])) + buff.append("") + buff.append("Class: %s" % get_cls_link(format)) + buff.append("") + + docstring = format.__doc__ + if docstring is not None: + docstring = cleandoc(docstring) + rst = str(SphinxDocString(docstring)) + buff.append(rst) + buff.append("") + + buff.append("Conversions") + buff.append("-----------") + methods = check_supported(format) + for method in methods: + buff.append("") + buff.append(f".. _{format.__name__}_{method}:") + buff.append("") + if method.startswith("from_"): + buff.append("Convert from this format to %s" % method_classes[method]) + buff.append("`" * len(buff[-1])) + elif method.startswith("to_"): + buff.append("Convert from %s to this format" % method_classes[method]) + buff.append("`" * len(buff[-1])) + buff.append("") + method_obj = getattr(format, method) + if ( + method == "to_labeled_system" + and method not in format.__dict__ + and "to_system" in format.__dict__ + ): + method_obj = getattr(format, "to_system") + docstring = method_obj.__doc__ + if docstring is not None: + docstring = cleandoc(docstring) + sig = signature(method_obj) + parameters = dict(sig.parameters) + return_annotation = sig.return_annotation + # del self + del parameters[list(parameters)[0]] + # del data + if method.startswith("to_"): + del parameters[list(parameters)[0]] + if "args" in parameters: + del parameters["args"] + if "kwargs" in parameters: + del parameters["kwargs"] + if method == "to_multi_systems" or method.startswith("from_"): + sig = Signature( + list(parameters.values()), return_annotation=method_cls_obj[method] + ) + else: + sig = Signature( + list(parameters.values()), return_annotation=return_annotation + ) + sig = str(sig) + if method.startswith("from_"): + if method != "from_multi_systems": + for aa in alias: + parameters["fmt"] = Parameter( + "fmt", + Parameter.POSITIONAL_OR_KEYWORD, + default=None, + annotation=Literal[aa], + ) + sig_fmt = Signature( + list(parameters.values()), + return_annotation=method_cls_obj[method], + ) + sig_fmt = str(sig_fmt) + buff.append( + f""".. py:function:: dpdata.{method_classes[method]}{sig_fmt}""" + ) + buff.append(""" :noindex:""") + for aa in alias: + buff.append( + """.. py:function:: dpdata.{}.from_{}{}""".format( + method_classes[method], + aa.replace("/", "_").replace(".", ""), + sig, + ) + ) + buff.append(""" :noindex:""") + buff.append("") + if docstring is None or method not in format.__dict__: + docstring = """ Convert this format to :class:`%s`.""" % ( + method_classes[method] + ) + doc_obj = SphinxDocString(docstring) + if len(doc_obj["Parameters"]) > 0: + doc_obj["Parameters"] = [ + xx + for xx in doc_obj["Parameters"] + if xx.name not in ("*args", "**kwargs") + ] + else: + if method == "from_multi_systems": + doc_obj["Parameters"] = [ + numpydoc_Parameter( + "directory", + "str", + ["directory of systems"], + ) + ] + doc_obj["Yields"] = [] + doc_obj["Returns"] = [ + numpydoc_Parameter("", method_classes[method], ["converted system"]) + ] + rst = " " + str(doc_obj) + buff.append(rst) + buff.append("") + elif method.startswith("to_"): + for aa in alias: + parameters = { + "fmt": Parameter( + "fmt", + Parameter.POSITIONAL_OR_KEYWORD, + annotation=Literal[aa], + ), + **parameters, + } + if method == "to_multi_systems": + sig_fmt = Signature( + list(parameters.values()), + return_annotation=method_cls_obj[method], + ) + else: + sig_fmt = Signature( + list(parameters.values()), + return_annotation=return_annotation, + ) + sig_fmt = str(sig_fmt) + buff.append( + f""".. py:function:: dpdata.{method_classes[method]}.to{sig_fmt}""" + ) + buff.append(""" :noindex:""") + for aa in alias: + buff.append( + """.. py:function:: dpdata.{}.to_{}{}""".format( + method_classes[method], + aa.replace("/", "_").replace(".", ""), + sig, + ) + ) + buff.append(""" :noindex:""") + buff.append("") + if docstring is None or ( + method not in format.__dict__ + and not ( + method == "to_labeled_system" + and method not in format.__dict__ + and "to_system" in format.__dict__ + ) + ): + docstring = "Convert :class:`%s` to this format." % ( + method_classes[method] + ) + doc_obj = SphinxDocString(docstring) + if len(doc_obj["Parameters"]) > 0: + doc_obj["Parameters"] = [ + xx + for xx in doc_obj["Parameters"][1:] + if xx.name not in ("*args", "**kwargs") + ] + else: + if method == "to_multi_systems": + doc_obj["Parameters"] = [ + numpydoc_Parameter( + "directory", + "str", + ["directory to save systems"], + ) + ] + if method == "to_multi_systems": + doc_obj["Yields"] = [] + doc_obj["Returns"] = [ + numpydoc_Parameter("", method_classes[method], ["this system"]) + ] + rst = " " + str(doc_obj) + buff.append(rst) + buff.append("") + buff.append("") + buff.append("") + + with open("formats/%s.rst" % format.__name__, "w") as rstfile: + rstfile.write("\n".join(buff)) + + if __name__ == "__main__": formats = get_formats() with open("formats.csv", "w", newline="") as csvfile: fieldnames = [ - "Class", + "Format", "Alias", - "Supported Functions", + "Supported Conversions", ] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) @@ -110,10 +341,11 @@ def check_supported(fmt: Format): for kk, vv in formats.items(): writer.writerow( { - "Class": get_cls_link(kk), + "Format": ":ref:`%s`" % kk.__name__, "Alias": "\n".join("``%s``" % vvv for vvv in vv), - "Supported Functions": "\n".join( - method_links[mtd] for mtd in check_supported(kk) + "Supported Conversions": "\n".join( + method_links[mtd].format(kk.__name__, mtd) + for mtd in check_supported(kk) ), } ) @@ -151,3 +383,4 @@ def check_supported(fmt: Format): "Alias": "\n".join("``%s``" % vvv for vvv in vv), } ) + generate_sub_format_pages(formats) diff --git a/pyproject.toml b/pyproject.toml index 5b555ad59..087d6ff9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ docs = [ 'm2r2', 'deepmodeling-sphinx>=0.1.1', 'sphinx-argparse', + 'rdkit', ] [tool.setuptools.packages.find]