Skip to content

Commit

Permalink
mdantic (#16)
Browse files Browse the repository at this point in the history
* mdantic

* path

* tables

* tabulate

* restore lane

* qcsk doc
  • Loading branch information
loriab authored Apr 24, 2024
1 parent c1a20b5 commit 2c6f0ae
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ jobs:
- mkdocs
- mkdocs-material
- mkdocstrings-python
- tabulate
EOF
cat export.yaml
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ coverage:
ignore:
- qcmanybody/tests/component_data
- qcmanybody/tests/ref_data
- qcmanybody/tests/generate_component_data.py
status:
project:
default:
Expand Down
14 changes: 14 additions & 0 deletions docs/extensions/mdantic_v1/__init__.py
Original file line number Diff line number Diff line change
@@ -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
174 changes: 174 additions & 0 deletions docs/extensions/mdantic_v1/mdantic.py
Original file line number Diff line number Diff line change
@@ -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"</br>{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)
96 changes: 96 additions & 0 deletions docs/qcschema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@

<!-- ==== Inputs ================================================================= -->

::: 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


<!-- ==== Protocols ============================================================== -->

<!--
::: qcmanybody.models.manybody_pydv1.ManyBodyProtocolEnum
::: qcmanybody.models.manybody_pydv1.ManyBodyProtocols
options:
show_root_heading: true
$pydantic: qcmanybody.models.manybody_pydv1.ManyBodyProtocols
-->


<!-- ==== Properties/Outputs ===================================================== -->

::: qcmanybody.models.ManyBodyResultProperties
options:
show_root_heading: true

$pydantic: qcmanybody.models.ManyBodyResultProperties


::: qcmanybody.models.ManyBodyResult
options:
show_root_heading: true

$pydantic: qcmanybody.models.ManyBodyResult


<!-- ==== Misc. ================================================================== -->

<!-- $pydantic: qcmanybody.models.manybody_pydv1.AtomicSpecification -->
<!--
AtomicSpecification
ResultsBase
SuccessfulResultBase
-->

<!--
options:
merge_init_into_class: false
group_by_category: false
# explicit members list so we can set order and include `__init__` easily
members:
- __init__
- molecule
- model_config
- model_computed_fields
- model_extra
- model_fields
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- copy
-->

::: qcmanybody.resize_gradient
options:
show_root_heading: true

26 changes: 19 additions & 7 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 2c6f0ae

Please sign in to comment.