Skip to content

Custom Modules

MoltPy's module system is fully extensible. You can create custom modules without modifying any core code.

Module Structure

Each module lives in src/moltpy/modules/<module_name>/ and contains:

my_module/
├── manifest.json          # Required: module metadata
├── module.py              # Optional: BaseModule subclass with hooks
├── templates/             # Jinja2 templates for generated files
│   └── app/
│       └── my_module.py.j2
├── snippets/              # Optional: fragments injected into shared files
│   └── pyproject.toml.j2
└── static/                # Optional: static files copied verbatim

Manifest Format

{
  "name": "my_module",
  "description": "What this module does",
  "category": "infrastructure",
  "implies": ["database"],
  "conflicts": ["other_module"],
  "default": false,
  "variables": {
    "my_setting": {
      "type": "str",
      "default": "default_value",
      "prompt": "Human-readable prompt"
    },
    "my_advanced_setting": {
      "type": "bool",
      "default": false,
      "prompt": "Enable advanced feature?",
      "advanced": true
    }
  }
}

Manifest Fields

Field Required Description
name Yes Module identifier (snake_case)
description Yes Short human-readable description
category Yes Grouping key for UI
implies No Auto-selected dependencies
conflicts No Modules that cannot coexist
default No Pre-selected in custom mode
variables No Per-module configuration variables

Variable Fields

Field Required Description
type Yes Variable type: str, int, bool, or choice
default Yes Default value used when the variable is skipped
prompt Yes Human-readable prompt text shown in the wizard
advanced No If true, the variable is only shown when the user enables "advanced configuration options". Defaults to false
choices No Required when type is choice. List of valid string options

Templates

Files ending in .j2 are processed as Jinja2 templates. The .j2 suffix is stripped in the output.

Template Context

Your templates have access to:

{{ project.name }}           # Project name
{{ project.description }}    # Project description
{{ project.python_version }} # Python version
{{ modules }}                # List of resolved module names
{{ modules_info }}           # Dict of module metadata and variables
{{ has_module('auth') }}     # Check if a module is included
{{ config }}                 # Full ProjectConfig instance

Example Template

# templates/app/core/my_module.py.j2
"""My custom module."""

from __future__ import annotations

from app.config import settings

{% if has_module('database') %}
from app.database import async_session
{% endif %}

MY_SETTING = settings.MY_MODULE_SETTING

Snippets

Snippets are small fragments appended to shared files. Use them for:

  • Adding dependencies to pyproject.toml
  • Adding environment variables to .env.example
  • Adding imports or routes to app/main.py

Snippet File Naming

The relative path within snippets/ determines the target file:

snippets/pyproject.toml.j2      → appends to pyproject.toml
snippets/.env.example.j2        → appends to .env.example
snippets/app/main.py.j2         → appends to app/main.py

Note: Snippets replace <!-- moltpy:SNIPPETS --> or # moltpy:SNIPPETS markers if present in the target file; otherwise they are appended at the end. For files like app/main.py where order matters, add a marker to the core template or use conditional blocks directly.

Example Snippet

{# snippets/pyproject.toml.j2 #}
{% if has_module('my_module') %}
    "my-library>=1.0",
{% endif %}

Static Files

Files in static/ are copied verbatim into the generated project. Use them for:

  • Configuration files
  • Assets
  • Scripts that do not need templating

Module Hooks

Modules can implement pre_render() and post_render() hooks by creating a module.py file with a BaseModule subclass:

from moltpy.modules.base import BaseModule

class MyModule(BaseModule):
    def pre_render(self, context: dict) -> dict:
        # Modify template context before rendering
        context["my_module_enabled"] = True
        return context

    def post_render(self, target_dir: Path, context: dict) -> None:
        # Perform additional file operations after rendering
        pass

External Modules

MoltPy can load modules from external sources without modifying core code.

Local External Modules

Place module directories in ~/.config/moltpy/modules/:

~/.config/moltpy/modules/
├── my_custom_module/
│   ├── manifest.json
│   ├── templates/
│   └── snippets/

These modules are auto-discovered alongside built-in modules.

GitHub Modules

Reference modules directly from GitHub repositories:

moltpy new --modules auth,github:user/repo#module-name --no-interactive

Format: github:<user>/<repo>#<module-dir-name>

With version pinning:

moltpy new --modules github:user/repo@v1.0.0#module-name --no-interactive

GitHub modules are cached in ~/.cache/moltpy/github_modules/. Use --refresh-modules to force a re-download.

Template Overrides

You can override built-in templates (core or module) without forking MoltPy:

moltpy new --preset standard --template-dir ./my-templates --no-interactive

Place override templates in the same relative path as the built-ins:

my-templates/
├── app/
│   └── main.py.j2              # Overrides core template
└── auth/
    └── templates/
        └── app/
            └── auth.py.j2      # Overrides auth module template

Override templates are validated for Jinja2 syntax before any files are written.

Testing Your Module

  1. Place your module in src/moltpy/modules/<name>/ (for built-in) or ~/.config/moltpy/modules/<name>/ (for external)
  2. Run moltpy modules list to verify it is discovered
  3. Run moltpy modules info <name> to verify the manifest
  4. Create a test project: moltpy new --modules <name> --no-interactive
  5. Use --dry-run to preview output without writing files

Module Best Practices

  1. Keep modules focused — One clear purpose per module
  2. Use implies — Do not force users to manually select dependencies
  3. Use has_module() — Make your templates conditional when depending on other modules
  4. Document variables — Clear prompts help users configure correctly
  5. Use advanced for power-user options — Keep the wizard simple for beginners by marking complex or niche variables as "advanced": true
  6. Handle missing config gracefully — Use defaults when variables are not set