File: _hooks.py

package info (click to toggle)
magicgui 0.9.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 21,796 kB
  • sloc: python: 11,202; makefile: 11; sh: 9
file content (227 lines) | stat: -rw-r--r-- 7,330 bytes parent folder | download
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![]({dest}){{: .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![]({dark}#only-dark){{: .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