Skip to content

Commit

Permalink
mdantic
Browse files Browse the repository at this point in the history
  • Loading branch information
loriab committed Apr 23, 2024
1 parent 4c65188 commit 96a7f43
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 11 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-latest
pytest: "-m 'not addon'"

- label: Py-max
python-version: "3.12"
runs-on: ubuntu-latest
pytest: "-k 'not (he4 and (3b or 4b))'"
#- label: Py-max
# python-version: "3.12"
# runs-on: ubuntu-latest
# pytest: "-k 'not (he4 and (3b or 4b))'"

name: "🐍 ${{ matrix.cfg.python-version }} • ${{ matrix.cfg.label }} • ${{ matrix.cfg.runs-on }}"
runs-on: ${{ matrix.cfg.runs-on }}
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)
Binary file modified docs/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions docs/qcschema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@


$pydantic: qcmanybody.models.manybody_pydv1.AtomicSpecification


$pydantic: qcmanybody.models.manybody_pydv1.ManyBodyInput

### ManyBodyKeywords


Pydantic models are simply classes which inherit from `BaseModel` and define fields as annotated attributes.

::: qcmanybody.models.ManyBodyKeywords
options:
show_root_heading: true
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:
- QCManyBody Docs: index.md
- tutorials.md
- API Documentation: api.md
- QCSchema: qcschema.md
- How-To Guides: how-to-guides.md
- reference.md
- explanation.md
Expand Down

0 comments on commit 96a7f43

Please sign in to comment.