Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mdantic #16

Merged
merged 6 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Loading