1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
|
"""https://www.mkdocs.org/dev-guide/plugins/#events ."""
from __future__ import annotations
import importlib.abc
import os
import sys
import types
import typing
import warnings
from contextlib import contextmanager
from importlib import import_module
from importlib.machinery import ModuleSpec
from itertools import count
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Mapping
from griffe.dataclasses import Alias
from griffe.docstrings import numpy
from mkdocstrings_handlers.python.handler import PythonHandler
from magicgui.type_map import get_widget_class
warnings.simplefilter("ignore", DeprecationWarning)
if TYPE_CHECKING:
from mkdocs.structure.pages import Page
# TODO: figure out how to do this with options
@contextmanager
def _hide_numpy_warn():
if not hasattr(numpy, "_warn"):
yield
return
before, numpy._warn = numpy._warn, lambda *x, **k: None
yield
numpy._warn = before
def inject_dynamic_docstring(item: Alias, identifier: str) -> None:
for i in range(1, 3):
module_name, *names = identifier.rsplit(".", 1)
try:
module = import_module(module_name)
except ModuleNotFoundError:
continue
else:
obj = module
for name in names:
obj = getattr(obj, name)
first_line, *rest = (obj.__doc__ or "").splitlines()
if first_line and item.target.docstring:
item.target.docstring.value = (
first_line + "\n" + dedent("\n".join(rest))
)
break
class WidgetHandler(PythonHandler):
def collect(self, identifier: str, config: Mapping[str, Any]) -> Any:
item = super().collect(identifier, config)
if isinstance(item, Alias):
inject_dynamic_docstring(item, identifier)
# to edit default in the parameter table
# item.parameters["something"].default = ...
return item
def render(self, data: Any, config: Mapping[str, Any]) -> str:
with _hide_numpy_warn():
return super().render(data, config)
class MyLoader(importlib.abc.Loader):
def create_module(self, spec):
return types.ModuleType(spec.name)
def exec_module(self, module: types.ModuleType) -> None:
def get_handler(
*,
theme: str,
custom_templates: str | None = None,
config_file_path: str | None = None,
paths: list[str] | None = None,
locale: str = "en",
load_external_modules: bool | None = None,
**config: Any,
) -> PythonHandler:
return WidgetHandler(
handler="python",
theme=theme,
custom_templates=custom_templates,
config_file_path=config_file_path,
paths=paths,
locale=locale,
load_external_modules=load_external_modules,
)
module.get_handler = get_handler
class Finder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname: str, *args, **kwargs) -> ModuleSpec | None:
if fullname == "mkdocstrings_handlers.widget_handler":
return ModuleSpec(fullname, MyLoader())
return None
def on_startup(**kwargs):
sys.meta_path.append(Finder())
def _replace_autosummary(md: str) -> str:
lines = md.splitlines()
start = lines.index("::: autosummary")
try:
last_line = lines.index("", start + 1)
except ValueError:
last_line = None
table = ["| Widget | Description |", "| ---- | ----------- |"]
for line in lines[start + 1 : last_line]:
name = line.strip()
if name:
module, _name = name.rsplit(".", 1)
obj = getattr(import_module(module), _name)
table.append(f"| [`{_name}`][{name}] | {obj.__doc__.splitlines()[0]} |")
lines[start:last_line] = table
return "\n".join(lines)
def _replace_type_to_widget(md: str) -> str:
lines = md.splitlines()
start = lines.index("::: type_to_widget")
try:
last_line = lines.index("", start + 1)
except ValueError:
last_line = None
table = [
"| <div style='width:210px'>Type Hint</div> "
"| <div style='width:100px'>Widget</div> "
"| `__init__` kwargs",
"| ---- | ------ | ------ |",
]
for line in lines[start + 1 : last_line]:
name = line.strip()
if name:
# eval Annotated types
hint = eval(name, typing.__dict__) if "Annotated" in name else name
wdg_type, kwargs = get_widget_class(annotation=hint)
kwargs.pop("nullable", None)
kwargs = f"`{kwargs}`" if kwargs else ""
wdg_name = f"magicgui.widgets.{wdg_type.__name__}"
wdg_link = f"[`{wdg_type.__name__}`][{wdg_name}]"
if "[" in name:
_name = name.split("[")[0]
name_link = f"[`{name}`][typing.{_name}]"
else:
name_link = f"[`{name}`][{name}]"
table.append(f"| {name_link} | {wdg_link} | {kwargs} | ")
lines[start:last_line] = table
return "\n".join(lines)
def _replace_widget_autosummary(md: str) -> str:
from magicgui import widgets
lines = md.splitlines()
start = lines.index("::: widget_autosummary")
try:
last_line = lines.index("", start + 1)
except ValueError:
last_line = None
autosummary = ["::: autosummary"]
for name in dir(widgets):
if name.startswith("_"):
continue
obj = getattr(widgets, name)
if (
isinstance(obj, type)
and issubclass(obj, widgets.Widget)
and obj is not widgets.Widget
and obj.__name__ == name
):
autosummary.append(f"magicgui.widgets.{name}")
lines[start:last_line] = autosummary
return "\n".join(lines)
def on_page_markdown(md: str, page: Page, **kwargs: Any) -> str:
"""Called when mkdocs is building the markdown for a page."""
import re
w_iter = count()
while "::: widget_autosummary" in md:
md = _replace_widget_autosummary(md)
while "::: autosummary" in md:
md = _replace_autosummary(md)
while "::: type_to_widget" in md:
md = _replace_type_to_widget(md)
def _add_images(matchobj: re.Match) -> str:
# add image links below all code blocks with a `.show()` call
if ".show()" not in matchobj.group(0):
return matchobj.group(0) or ""
src = matchobj.group(1) # source code
reldepth = "../" * page.file.src_path.count(os.sep) # relative of this file
dest = f"{reldepth}_images/{page.file.name}_{next(w_iter)}.png" # link to img
link = f"\n{{: .code-image}}\n\n" # theme aware link
# Not working ... theme aware
# light = dest.replace(".png", "_light.png")
# dark = dest.replace(".png", "_light.png")
# new_md += f"\n{{: .code-image}}\n\n"
new_md: str = "```python\n" + src + "\n```" + link
return new_md.replace(" # leave open", "")
if page.title not in {"migration guide"}:
md = re.sub("``` ?python\n([^`]*)```", _add_images, md, re.DOTALL)
return md
|