executablebooks / mystmd

Command line tools for working with MyST Markdown.
https://mystmd.org/guide
MIT License
172 stars 50 forks source link

:snake: Add support for Pyodide-backed plugins #1325

Open agoose77 opened 2 weeks ago

agoose77 commented 2 weeks ago

Fixes #1298

I spoke with @rowanc1 about this at Project Pythia's Cookoff. Whilst it wasn't a priority for either of us, I had some head space to get it on paper quickly and committed to it.

An example Python plugin

import json
import pyodide.ffi

def directive_run(data):
    query = data.arg
    size = getattr(data.options, "size", "500x200")
    url = f"https://source.unsplash.com/random/{size}/?{query}"
    img = {"type": "image", "url": url}
    return [img]

directive = {
    "name": "unsplash",
    "doc": "An example directive for showing a nice random image at a custom size.",
    "alias": ["random-pic"],
    "arg": {"type": "string"},
    "run": directive_run,
}

def transformer(opts, utils):
    def impl(node):
        images = utils.selectAll("image", node).to_py()
        print("SPLASH!", json.dumps(images, indent=2))
        return node
    return impl

transform = {
  "name": "unsplash",
  "plugin": transformer,
  "stage": "document"
}

plugin = {"directives": [directive], "transforms": [transform]}

It can be seen that the plugin structure directly matches the JS structure. However, there are some finer points around proxy conversion that could be cleaner.

The real change in this PR is making mystmd a commonjs package. I'm not totally au-fait with the details yet, but effectively the esbuild mjs->cjs conversion breaks the pyodide loading. I think pyodide links in the mjs bundle which then fails to load.

More digging required.

rowanc1 commented 2 weeks ago

I am still quite unclear on what benefits pyodide gives us? As this is in the context of a command-line tool, we can easily ensure access to python and the solution that you provided via a separate process and simple comms (stdin/out) works pretty well, and works today?

If we follow this path, I foresee that we will get into packaging of dependent python libraries rather fast, and having that as something our team has to manage/support seems like a lot of work.

Is the thought behind this work that we can support the plugin exports via python? If that is the case, going to a data-driven YAML format could be another way to solve it?

rowanc1 commented 2 weeks ago

Just to be clear -- this work is pretty amazing, and indicating that we have core python support for plugins should be a guide.

agoose77 commented 2 weeks ago

The idea here is that people who know how to manipulate basic data structures in Python can write simple plugins. The benefit is that we control the environment and do not need to worry about env management. Moreover, not all myst projects will need a python kernel. Using pyodide, we can keep the environment straightforward to define.

I'm not worried about python dependencies: ideally most myst plugins should be achievable with the standard lib.

An unexpected benefit of using pyodide is that we can pass in e.g unified utilities to the python plugin - we don't have to implement them ourselves! There is some care that needs to be taken with proxy conversion, I think most users won't need to worry about it.

Ultimately this isn't huge. I also think a process based approach should be implemented too, which uses the stdin/out approach.