diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bf92ea..fd52d69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,6 +157,7 @@ jobs: - mkdocs - mkdocs-material - mkdocstrings-python + - tabulate EOF cat export.yaml @@ -179,7 +180,7 @@ jobs: - name: Build Documentation run: | python -m pip install . --no-deps - mkdocs build + PYTHONPATH=docs/extensions mkdocs build cd docs - name: GitHub Pages Deploy diff --git a/codecov.yml b/codecov.yml index 051bda6..58cb87b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,6 +2,7 @@ coverage: ignore: - qcmanybody/tests/component_data - qcmanybody/tests/ref_data + - qcmanybody/tests/generate_component_data.py status: project: default: diff --git a/docs/extensions/mdantic_v1/__init__.py b/docs/extensions/mdantic_v1/__init__.py new file mode 100644 index 0000000..cbe0d1a --- /dev/null +++ b/docs/extensions/mdantic_v1/__init__.py @@ -0,0 +1,14 @@ +from .mdantic import ( + Mdantic, + analyze, + Field, + get_related_enum, + get_enum_values, + get_related_enum_helper, + mk_struct, + fmt_tab, + MdanticPreprocessor, + makeExtension, +) + +#from .samples import SampleModel diff --git a/docs/extensions/mdantic_v1/mdantic.py b/docs/extensions/mdantic_v1/mdantic.py new file mode 100644 index 0000000..6406476 --- /dev/null +++ b/docs/extensions/mdantic_v1/mdantic.py @@ -0,0 +1,174 @@ +import re +import inspect +import importlib +from enum import Enum +from collections import namedtuple +from typing import List, Dict, Optional + +import tabulate +from pydantic.v1 import BaseModel +from markdown import Markdown +from markdown.extensions import Extension +from markdown.preprocessors import Preprocessor + + +class Mdantic(Extension): + def __init__(self, configs=None): + if configs is None: + configs = {} + self.config = { + "init_code": ["", "python code to run when initializing"], + "columns": [ + ["key", "type", "required", "description", "default"], + "Columns to use in table, comma separated list", + ], + } + for key, value in configs.items(): + self.setConfig(key, value) + super().__init__() + + def extendMarkdown(self, md: Markdown) -> None: + md.preprocessors.register(MdanticPreprocessor(md, self.getConfigs()), "mdantic", 100) + + +Field = namedtuple("Field", "key type required description default") + + +def analyze(cls_name: str) -> Optional[Dict[str, List[Field]]]: + paths = cls_name.rsplit(".", 1) + if len(paths) != 2: + return None + + module = paths[0] + attr = paths[1] + try: + mod = importlib.import_module(module) + except ModuleNotFoundError: + return None + if not hasattr(mod, attr): + return None + + cls = getattr(mod, attr) + + if not issubclass(cls, BaseModel): + return None + + structs = {} + mk_struct(cls, structs) + return structs + + +def get_related_enum(ty: type): + visited = set() + result = [] + + get_related_enum_helper(ty, visited, result) + + return result + + +def get_enum_values(e): + return [x.value for x in list(e)] + + +def get_related_enum_helper(ty, visited, result): + visited.add(ty) + if inspect.isclass(ty) and issubclass(ty, Enum) and ty not in result: + result.append(ty) + + if hasattr(ty, "__args__"): + for sub_ty in getattr(ty, "__args__"): + if sub_ty not in visited: + get_related_enum_helper(sub_ty, visited, result) + + +# v1: +def mk_struct(cls: type[BaseModel], structs: Dict[str, List[Field]]) -> None: + this_struct: List[Field] = [] + structs[cls.__name__] = this_struct + # v2: for field_name, f in cls.model_fields.items(): + for field_name, f in cls.__fields__.items(): + title = f.field_info.title or field_name + annotation = str(f.type_) + description = "" if f.field_info.description is None else f.field_info.description + + if annotation is None: + return None + + related_enums = get_related_enum(annotation) + if related_enums: + for e in related_enums: + description += f"
{e.__name__}: {get_enum_values(e)}" + + default = f.get_default() + default = None if str(default) == "PydanticUndefined" else str(default) + + if hasattr(annotation, "__origin__"): + ty = str(annotation) + elif hasattr(annotation, "__name__"): + ty = annotation.__name__ + else: + ty = str(annotation) + + this_struct.append( + Field( + title, + ty, + # v2: str(f.is_required()), + str(f.required), + description, + default, + ) + ) + if hasattr(annotation, "__mro__"): + if BaseModel in annotation.__mro__: + mk_struct(annotation, structs) + + +def fmt_tab(structs: Dict[str, List[Field]], columns: List[str]) -> Dict[str, str]: + tabs = {} + for cls, struct in structs.items(): + tab = [] + for f in struct: + tab.append([getattr(f, name) for name in columns]) + tabs[cls] = tabulate.tabulate(tab, headers=columns, tablefmt="github") + return tabs + + +class MdanticPreprocessor(Preprocessor): + """ + This provides an "include" function for Markdown, similar to that found in + LaTeX (also the C pre-processor and Fortran). The syntax is {!filename!}, + which will be replaced by the contents of filename. Any such statements in + filename will also be replaced. This replacement is done prior to any other + Markdown processing. All file-names are evaluated relative to the location + from which Markdown is being called. + """ + + def __init__(self, md: Markdown, config): + super(MdanticPreprocessor, self).__init__(md) + self.init_code = config["init_code"] + if self.init_code: + exec(self.init_code) + self.columns = config["columns"] + + def run(self, lines: List[str]): + for i, l in enumerate(lines): + g = re.match(r"^\$pydantic: (.*)$", l) + if g: + cls_name = g.group(1) + structs = analyze(cls_name) + if structs is None: + print(f"warning: mdantic pattern detected but failed to process or import: {cls_name}") + continue + tabs = fmt_tab(structs, self.columns) + table_str = "" + for cls, tab in tabs.items(): + table_str += "\n" + f"**{cls}**" + "\n\n" + str(tab) + "\n" + lines = lines[:i] + [table_str] + lines[i + 1 :] + + return lines + + +def makeExtension(*_, **kwargs): + return Mdantic(kwargs) diff --git a/docs/qcschema.md b/docs/qcschema.md new file mode 100644 index 0000000..098d860 --- /dev/null +++ b/docs/qcschema.md @@ -0,0 +1,96 @@ + + + +::: qcmanybody.models.BsseEnum + + +::: qcmanybody.models.ManyBodyKeywords + options: + show_root_heading: true + +$pydantic: qcmanybody.models.manybody_pydv1.ManyBodyKeywords + + +::: qcmanybody.models.manybody_pydv1.ManyBodySpecification + options: + show_root_heading: true + +$pydantic: qcmanybody.models.manybody_pydv1.ManyBodySpecification + + +::: qcmanybody.models.ManyBodyInput + options: + show_root_heading: true + +$pydantic: qcmanybody.models.manybody_pydv1.ManyBodyInput + + + + + + + + + +::: qcmanybody.models.ManyBodyResultProperties + options: + show_root_heading: true + +$pydantic: qcmanybody.models.ManyBodyResultProperties + + +::: qcmanybody.models.ManyBodyResult + options: + show_root_heading: true + +$pydantic: qcmanybody.models.ManyBodyResult + + + + + + + + + +::: qcmanybody.resize_gradient + options: + show_root_heading: true + diff --git a/mkdocs.yml b/mkdocs.yml index 0f16c4c..f6543bf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,19 +42,31 @@ plugins: options: docstring_style: numpy allow_inspection: true + members_order: source + separate_signature: true + filters: ["!^_"] + docstring_options: + ignore_init_summary: true + merge_init_into_class: true + show_signature_annotations: true + signature_crossrefs: true import: - - https://docs.python.org/3.12/objects.inv - - https://numpy.org/doc/stable/objects.inv - - https://docs.scipy.org/doc/scipy/objects.inv - - https://matplotlib.org/stable/objects.inv - - https://molssi.github.io/QCElemental/objects.inv - - https://molssi.github.io/QCEngine/objects.inv - - https://molssi.github.io/QCFractal/objects.inv + - https://docs.python.org/3/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://docs.scipy.org/doc/scipy/objects.inv + - https://matplotlib.org/stable/objects.inv + - https://molssi.github.io/QCElemental/objects.inv + - https://molssi.github.io/QCEngine/objects.inv + - https://molssi.github.io/QCFractal/objects.inv + +markdown_extensions: + - mdantic_v1 nav: - Home: index.md - high-level-interface.md - core-interface.md + - QCSchema: qcschema.md - How-To Guides: how-to-guides.md - API Documentation: api.md