Skip to content
Bundler plugins

Bundler plugins

Plugins extend bunpy build with custom logic at two points in the bundle pipeline: the resolve hook intercepts module specifiers before bunpy decides what file to load, and the transform hook processes the raw file content before it is added to the bundle. Both hooks are written in Python and registered in pyproject.toml.

Plugin structure

A plugin is a Python module that exports a Plugin dataclass and a setup function:

# myplugin.py
from bunpy.build import Plugin, BuildContext, ResolveArgs, TransformArgs

plugin = Plugin(name="myplugin", version="0.1.0")

def setup(ctx: BuildContext) -> None:
    ctx.on_resolve(on_resolve)
    ctx.on_transform(on_transform)

def on_resolve(args: ResolveArgs) -> str | None:
    # Return a file path to handle this import, or None to pass through
    return None

def on_transform(args: TransformArgs) -> str | None:
    # Return transformed source code, or None to pass through
    return None

Register the plugin in pyproject.toml:

[tool.bunpy.build]
plugins = ["myplugin:plugin"]

The string "myplugin:plugin" follows the module:attribute pattern - bunpy imports myplugin and reads its plugin attribute.

Resolve hook

The resolve hook fires for every import statement bunpy encounters during the static trace. Use it to redirect imports for non-standard modules:

def on_resolve(args: ResolveArgs) -> str | None:
    # args.specifier: the import string, e.g. "myapp.config"
    # args.importer: absolute path of the file containing the import
    # args.kind:     "import" | "from_import"

    if args.specifier.endswith(".toml"):
        # Map "config.toml" to its absolute path
        import os
        base = os.path.dirname(args.importer)
        return os.path.join(base, args.specifier)

    return None   # let bunpy resolve normally

Transform hook

The transform hook fires after a file is read, before it is stored in the bundle. Return modified source code or None to leave the file unchanged:

def on_transform(args: TransformArgs) -> str | None:
    # args.path:     absolute path of the file being processed
    # args.contents: raw file contents as a string
    # args.loader:   file extension, e.g. ".py" or ".toml"

    if args.loader != ".toml":
        return None

    import tomllib, json
    data = tomllib.loads(args.contents)
    # Emit a .py module that exposes the TOML data as a dict
    return f"data = {json.dumps(data, indent=2)}\n"

Example: .toml imports

This full plugin lets you write from myapp import settings where settings maps to settings.toml:

# bunpy_toml_plugin.py
import os
import tomllib
import json
from bunpy.build import Plugin, BuildContext, ResolveArgs, TransformArgs

plugin = Plugin(name="toml-importer", version="0.1.0")

def setup(ctx: BuildContext) -> None:
    ctx.on_resolve(on_resolve)
    ctx.on_transform(on_transform)

def on_resolve(args: ResolveArgs) -> str | None:
    if not args.specifier.endswith(".toml"):
        return None
    base = os.path.dirname(args.importer)
    candidate = os.path.normpath(os.path.join(base, args.specifier))
    if os.path.isfile(candidate):
        return candidate
    return None

def on_transform(args: TransformArgs) -> str | None:
    if args.loader != ".toml":
        return None
    data = tomllib.loads(args.contents)
    return f"# generated by toml-importer\ndata = {json.dumps(data, indent=2)}\n"
# pyproject.toml
[tool.bunpy.build]
plugins = ["bunpy_toml_plugin:plugin"]
# src/myapp/main.py
from . import settings  # resolves to settings.toml
print(settings.data["database"]["host"])

Example: Jinja2 template embedding

Embed .jinja2 templates as precompiled Python strings so they are included in the bundle:

# bunpy_jinja_plugin.py
from bunpy.build import Plugin, BuildContext, TransformArgs

plugin = Plugin(name="jinja2-loader", version="0.1.0")

def setup(ctx: BuildContext) -> None:
    ctx.on_transform(on_transform)

def on_transform(args: TransformArgs) -> str | None:
    if args.loader != ".jinja2":
        return None
    # Expose the template source as a module-level string
    escaped = args.contents.replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
    return f'template_source = """{escaped}"""\n'

In application code:

from jinja2 import Environment
from . import email_template   # email_template.jinja2

env = Environment()
tmpl = env.from_string(email_template.template_source)
print(tmpl.render(name="Alice"))

Example: Version injection

Inject the package version from pyproject.toml into the bundle at build time, so the shipped binary always knows its own version:

# bunpy_version_plugin.py
import tomllib
import os
from bunpy.build import Plugin, BuildContext, TransformArgs

plugin = Plugin(name="version-inject", version="0.1.0")
_project_version: str = ""

def setup(ctx: BuildContext) -> None:
    global _project_version
    pyproject_path = os.path.join(ctx.root, "pyproject.toml")
    with open(pyproject_path, "rb") as f:
        data = tomllib.load(f)
    _project_version = data["project"]["version"]
    ctx.on_transform(on_transform)

def on_transform(args: TransformArgs) -> str | None:
    if args.loader != ".py":
        return None
    if "__VERSION__" not in args.contents:
        return None
    return args.contents.replace("__VERSION__", repr(_project_version))
# src/myapp/__init__.py
__version__ = __VERSION__   # replaced with "1.2.3" at build time

Plugin registration reference

[tool.bunpy.build]
plugins = [
  "bunpy_toml_plugin:plugin",
  "bunpy_jinja_plugin:plugin",
  "bunpy_version_plugin:plugin",
]

Plugins run in declaration order. The resolve hooks run in order until one returns a non-None value. The transform hooks run in order and chain - the output of hook N is passed as args.contents to hook N+1.

TransformArgs and ResolveArgs reference

ResolveArgs

FieldTypeDescription
specifierstrThe import string from the source file
importerstrAbsolute path of the file containing the import
kindstr"import" or "from_import"

TransformArgs

FieldTypeDescription
pathstrAbsolute path of the file
contentsstrRaw file contents
loaderstrFile extension including the dot, e.g. ".py"

BuildContext

MethodDescription
ctx.on_resolve(fn)Register a resolve hook
ctx.on_transform(fn)Register a transform hook
ctx.rootAbsolute path to the project root