Skip to main content

Laravel-modules inspired modular architecture for Python — framework agnostic.

Project description

pymodules

Modular application architecture for Python — inspired by Laravel Modules, built for the Python ecosystem.

Version Python License: MIT Status

pymodules lets you split large Python applications into self-contained, independently toggleable modules — each with its own models, views, routes, config, migrations, service providers, and tests. It works with any Python project and ships with first-class support for Django, Flask, and FastAPI.


Table of Contents


Why pymodules?

A typical Django or FastAPI project starts flat and becomes a maze:

myproject/
    models.py          # 800 lines, everything mixed together
    views.py           # 1200 lines
    urls.py            # references all 40 views
    settings.py        # references everything

pymodules enforces a boundary-first architecture. Every feature is a module:

myproject/
    modules/
        Blog/           # completely self-contained
        Shop/           # independent — can be disabled with one line
        Auth/           # has its own models, routes, config, migrations
        Notifications/

Each module is a Python package that can be enabled or disabled at runtime by flipping a flag in its module.json. Disabled modules are not imported, not booted, and not registered with your framework.

This is the Python equivalent of nWidart/laravel-modules — the most popular Laravel package for large application architecture.


Version 0.1.0

This is the first public release of pymodules. It is production-usable for greenfield projects and stable enough for evaluation in existing projects.

What 0.1.0 delivers:

  • Framework-agnostic core (ModuleRegistry, Module, ServiceProvider)
  • Auto-detection of Django, FastAPI, Flask from your environment
  • pymodules CLI with init, make, list, enable, disable, show, delete, publish, detect, presets
  • Django manage.py integration (module_make, module_list, module_enable, module_disable, module_show, module_delete, module_publish)
  • Eight scaffold presets: plain, default, django, django-api, fastapi, fastapi-crud, flask, flask-api
  • Per-project config via pymodules.toml
  • Custom folder naming — call it modules/, plugins/, apps/, src/features/ — anything
  • Module manifest (module.json) with versioning, enable/disable, provider registration
  • Service Provider pattern for boot-time registration logic
  • Module-level settings merged into Django settings
  • Module-level URL routing for all three frameworks
  • Module-level migrations for Django
  • Asset publishing system

Installation

# Core — no framework dependencies (pure Python, FastAPI, Flask)
pip install pytmodules

# With Django support
pip install "pytmodules[django]"

# With Django + Django REST Framework scaffolding support
pip install "pytmodules[django-api]"

# With Flask support
pip install "pytmodules[flask]"

# With FastAPI support
pip install "pytmodules[fastapi]"

# Everything
pip install "pytmodules[django,flask,fastapi]"

From GitHub (development installs):

pip install git+https://github.com/tomcroot/pymodules.git
pip install "pytmodules[django] @ git+https://github.com/tomcroot/pymodules.git"

Requirements: Python 3.10+

Release Process

Releases are automated with GitHub Actions:

Local validation before cutting a release:

python -m build
python -m twine check dist/*

Release flow:

  1. Ensure CHANGELOG.md has release-ready notes under [Unreleased].
  2. Run the Release workflow from GitHub Actions (workflow_dispatch) and provide the version (for example, 0.1.1).
  3. The workflow updates version metadata, commits, tags (v0.1.1), and creates the GitHub Release.
  4. Tag push automatically triggers the publish workflow, which builds and uploads to PyPI.

The publish workflow is configured for PyPI trusted publishing. Keep the GitHub pypi environment and PyPI trusted publisher settings in sync if repository, workflow, or environment names change.


Quick Start

Any Python project

cd myproject
pymodules init          # auto-detects your framework, creates pymodules.toml
pymodules make Blog     # scaffold a Blog module using detected framework

Django project

pip install "pytmodules[django]"
pymodules init          # detects Django, writes pymodules.toml
pymodules make Blog     # creates full Django module scaffold

# Or with manage.py (once "pymodules" is in INSTALLED_APPS):
python manage.py module_make Blog

FastAPI project

pip install "pytmodules[fastapi]"
pymodules init
pymodules make Blog     # creates FastAPI module with APIRouter + Pydantic schemas

Flask project

pip install "pytmodules[flask]"
pymodules init
pymodules make Blog     # creates Flask module with Blueprint

Module Structure

Every module is a Python package directory inside your modules folder. The layout depends on the scaffold preset used, but the full structure (from the django preset) is:

modules/
└── Blog/
    ├── module.json               ← required: manifest, version, enabled flag, providers
    ├── __init__.py
    ├── apps.py                   ← Django AppConfig (django preset)
    ├── providers.py              ← ServiceProvider subclass
    ├── routes.py                 ← URL patterns / Blueprint / APIRouter
    ├── admin.py                  ← Django admin registration (django preset)
    ├── serializers.py            ← DRF serializers, commented (django preset)
    ├── schemas.py                ← Pydantic schemas (fastapi preset)
    ├── services.py               ← Business logic layer (fastapi/flask presets)
    ├── models/
    │   ├── __init__.py
    │   └── blog.py               ← Django model (django preset)
    ├── views/
    │   ├── __init__.py
    │   └── blog_views.py         ← Django views (django preset)
    ├── config/
    │   ├── __init__.py
    │   └── config.py             ← Module-level settings (all presets)
    ├── database/
    │   └── migrations/           ← Django migrations (django preset)
    │       └── __init__.py
    ├── assets/                   ← Publishable static files
    └── tests/
        ├── __init__.py
        └── test_blog.py          ← Starter test class

The module.json Manifest

Every module has a module.json at its root. This is the single source of truth for the module's identity and state.

{
    "name": "Blog",
    "version": "1.0.0",
    "description": "Blog module with posts, tags and comments.",
    "author": "Your Name",
    "enabled": true,
    "requires": ["Core"],
    "providers": [
        "modules.Blog.providers.BlogServiceProvider"
    ],
    "publishes": {
        "default": {
            "assets/blog.css": "static/blog/blog.css"
        },
        "config": {
            "config/config.py": "config/blog_config.py"
        }
    }
}
Field Required Description
name Yes Module name (PascalCase)
version No Semantic version string
description No Human-readable description
author No Author name or email
enabled Yes true/false — controls whether the module boots
requires No List of module names that must boot before this module
providers No Dotted paths to ServiceProvider subclasses
publishes No Asset publish map (group → source → destination)

CLI Reference — pymodules

The pymodules CLI is available after installation. All commands read pymodules.toml for defaults.

pymodules init

Initialise pymodules in a project. Auto-detects your framework and writes pymodules.toml.

pymodules init                             # fully automatic
pymodules init --path plugins              # custom folder name
pymodules init --path apps --preset django # explicit preset
pymodules init --force                     # overwrite existing config

pymodules detect

Show which framework was detected and why.

pymodules detect
pymodules detect --path /other/project

Output:

  Framework  : django
  Preset     : django
  Confidence : high
  Reason     : django is installed in the active Python environment;
               found django-specific project files (manage.py)

pymodules make

Create a new module scaffold.

pymodules make Blog                        # auto-detected preset
pymodules make Blog --preset django        # explicit preset
pymodules make Blog --preset fastapi
pymodules make Blog --preset flask
pymodules make Blog --preset plain         # bare minimum
pymodules make Blog --force                # overwrite existing

pymodules list

List all modules and their status.

pymodules list
pymodules list --enabled-only
pymodules list --disabled-only

pymodules enable / pymodules disable

Toggle a module on or off. Writes to module.json — persists across restarts.

pymodules enable  Blog
pymodules disable Blog

pymodules show

Show full details for a single module.

pymodules show Blog

pymodules delete

Delete a module from disk.

pymodules delete Blog
pymodules delete Blog --yes    # skip confirmation

pymodules publish

Copy a module's publishable assets to the host application.

pymodules publish Blog
pymodules publish Blog --group config
pymodules publish              # publish all enabled modules
pymodules publish --force      # overwrite existing files

pymodules presets

List all available scaffold presets.

pymodules presets

Global options

pymodules --modules-path src/apps make Blog   # override modules folder

Set PYMODULES_PATH environment variable to avoid passing --modules-path on every command.


Framework Detection

pymodules detects your framework automatically using a three-layer strategy:

Layer Method Confidence boost
1 importlib.util.find_spec() — is the package installed in this venv? +10
1b DJANGO_SETTINGS_MODULE environment variable set? +5 (Django only)
2 Project file fingerprints (manage.py, wsgi.py, app.py …) +3
3 Dependency file scanning (requirements.txt, pyproject.toml, Pipfile, setup.cfg) +2

When multiple frameworks are detected, priority is Django > FastAPI > Flask.

Use the detector in your own code:

from pymodules import detect_framework

info = detect_framework()
print(info.name)        # "django" | "fastapi" | "flask" | "unknown"
print(info.confidence)  # "high" | "medium" | "low"
print(info.preset)      # matching preset name
print(info.reason)      # human-readable explanation

Scaffold Presets

Presets control exactly which files are generated by pymodules make.

plain

Bare minimum. Use when you want to build your own structure.

Blog/
  module.json
  __init__.py

default

Framework-agnostic. A clean starting point for any Python project.

Blog/
  module.json, __init__.py, providers.py
  config/config.py
  tests/test_blog.py
  assets/

django

Full Django module. Everything you need to add a new bounded context.

Blog/
  module.json, __init__.py, apps.py, providers.py
  models/blog.py          ← ready-to-use Django model
  views/blog_views.py     ← index + detail views
  routes.py               ← urlpatterns + prefix
  admin.py                ← commented ModelAdmin
  serializers.py          ← commented DRF serializer
  config/config.py
  database/migrations/__init__.py
  tests/test_blog.py
  assets/

django-api

Django REST module with DRF serializer, viewset, and router wiring.

Blog/
    module.json, __init__.py, apps.py, providers.py
    models/blog.py
    serializers.py      ← DRF ModelSerializer + ListSerializer
    policies.py         ← DRF AccessPolicy scaffold for API permissions
    viewsets.py         ← DRF ModelViewSet
    api/urls.py         ← DefaultRouter wiring
    routes.py           ← thin wrapper delegating to api/urls.py
    admin.py
    config/config.py
    database/migrations/__init__.py
    tests/test_blog.py
    assets/

fastapi

FastAPI module with 3-endpoint API skeleton.

Blog/
  module.json, __init__.py, providers.py
  routes.py       ← APIRouter with list/get/create endpoints
  schemas.py      ← Pydantic BaseModel (Schema + CreateSchema)
  services.py     ← Service class (business logic layer)
  config/config.py
  tests/test_blog.py

fastapi-crud

FastAPI module with full CRUD endpoints.

Blog/
    module.json, __init__.py, providers.py
    routes.py       ← APIRouter with list/get/create/update/delete endpoints
    schemas.py      ← Schema + CreateSchema + UpdateSchema
    services.py     ← includes update() and delete() skeletons
    config/config.py
    tests/test_blog.py

flask

Flask module with Blueprint.

Blog/
  module.json, __init__.py, providers.py
  routes.py       ← Blueprint with index + detail routes
  services.py     ← Service class
  config/config.py
  tests/test_blog.py

flask-api

Flask module with JSON REST CRUD endpoints.

Blog/
    module.json, __init__.py, providers.py
    routes.py       ← /api/{module}/ CRUD Blueprint
    services.py     ← all/create/find/update/delete skeletons
    config/config.py
    tests/test_blog.py

Service Providers

Service providers run arbitrary boot logic when a module is loaded. They are the correct place to register signals, middleware, event listeners, DI bindings, or any other setup that needs to run once at startup.

# modules/Blog/providers.py
from pymodules import ServiceProvider

class BlogServiceProvider(ServiceProvider):

    def register(self) -> None:
        """
        Called during module boot.
        Register bindings, connect signals, set up DI.
        """
        from django.db.models.signals import post_save
        from .models.blog import Post
        from .listeners import on_post_saved
        post_save.connect(on_post_saved, sender=Post)

    def boot(self) -> None:
        """
        Called after all modules have registered.
        Use for setup that depends on other modules.
        """
        pass

Reference providers in module.json:

{
    "providers": [
        "modules.Blog.providers.BlogServiceProvider"
    ]
}

Django Integration

settings.py

from pathlib import Path
from pymodules.integrations.django import DjangoModuleRegistry

BASE_DIR = Path(__file__).resolve().parent.parent

MODULE_REGISTRY = DjangoModuleRegistry(modules_path=BASE_DIR / "modules")

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    # ...

    "pymodules",                          # registers manage.py module_* commands
    *MODULE_REGISTRY.installed_apps(),    # auto-registers all enabled modules
]

# Point Django to each module's own migrations folder
MIGRATION_MODULES = MODULE_REGISTRY.migration_modules()

# Merge UPPER_CASE variables from every module's config/config.py
locals().update(MODULE_REGISTRY.collect_settings())

urls.py

from django.contrib import admin
from django.urls import path
from django.conf import settings

urlpatterns = [
    path("admin/", admin.site.urls),
    *settings.MODULE_REGISTRY.url_patterns(),   # auto-includes all module routes
    *settings.MODULE_REGISTRY.api_url_patterns(),   # auto-includes /api/<module>/ routes
]

Module routes.py

# modules/Blog/routes.py
from django.urls import path
from .views.blog_views import index, detail

prefix = "blog"     # mounted at /blog/

app_name = "blog"

urlpatterns = [
    path("", index, name="index"),
    path("<int:pk>/", detail, name="detail"),
]

Module-level settings

# modules/Blog/config/config.py
BLOG_POSTS_PER_PAGE = 10
BLOG_ALLOW_COMMENTS = True
BLOG_CACHE_TTL = 300

These are automatically merged into Django settings via collect_settings().

API permissions with collect_policies()

For DRF-based modules, DjangoModuleRegistry.collect_policies() auto-discovers AccessPolicy subclasses from enabled modules. This is library-level plumbing: pymodules handles discovery and naming conventions, while each application keeps its actual permission rules inside its own modules.

collect_policies() supports either:

  • a flat policies.py file for simple modules
  • a policies/ package for larger modules

Example settings.py usage:

from pathlib import Path
from pymodules.integrations.django import DjangoModuleRegistry

BASE_DIR = Path(__file__).resolve().parent.parent
MODULE_REGISTRY = DjangoModuleRegistry(modules_path=BASE_DIR / "modules")

MODULE_POLICIES = MODULE_REGISTRY.collect_policies()

Example django-api scaffold output:

# modules/Blog/policies.py
from rest_framework_access_policy import AccessPolicy


class BlogPolicy(AccessPolicy):
    statements = [
        {
            "action": ["list", "retrieve"],
            "principal": "authenticated",
            "effect": "allow",
        },
    ]

Direct binding inside a DRF viewset:

from rest_framework import viewsets

from .models.blog import Blog
from .policies import BlogPolicy
from .serializers import BlogSerializer


class BlogViewSet(viewsets.ModelViewSet):
    queryset = Blog.objects.all()
    serializer_class = BlogSerializer
    permission_classes = [BlogPolicy]

Registry lookup when you want indirection:

from django.conf import settings

BlogPolicy = settings.MODULE_REGISTRY.get_policy("modules.Blog.policies.BlogPolicy")

If you use a policies/ package, keys reflect the real import path, for example modules.HR.policies.employee.EmployeePolicy.

This feature requires drf-access-policy, which is included in pytmodules[django-api].

manage.py commands

Once "pymodules" is in INSTALLED_APPS, all module_* commands are available:

python manage.py module_make   Blog
python manage.py module_make   Blog --preset django
python manage.py module_make   Blog --force
python manage.py module_list
python manage.py module_list   --enabled
python manage.py module_list   --disabled
python manage.py module_show   Blog
python manage.py module_enable  Blog
python manage.py module_disable Blog
python manage.py module_delete  Blog
python manage.py module_delete  Blog --yes
python manage.py module_publish Blog
python manage.py module_publish Blog --group config
python manage.py module_publish --force
python manage.py module_make_model Blog Post
python manage.py module_make_model Blog ArchivedPost --proxy --parent Post
python manage.py module_make_serializer Blog Post
python manage.py module_make_viewset Blog Post
python manage.py module_make_api_urls Blog
python manage.py module_make_model_migration Blog Post --auto-name
python manage.py module_make_migration Blog
python manage.py module_migrate Blog

module_make without --preset defaults to "django" when running via manage.py — because the fact that you're using manage.py is itself proof this is a Django project.


Flask Integration

from flask import Flask
from pymodules.integrations.flask import FlaskModuleRegistry

app = Flask(__name__)
registry = FlaskModuleRegistry(modules_path="modules", app=app)
registry.boot()  # auto-registers all enabled module Blueprints

App factory pattern:

registry = FlaskModuleRegistry(modules_path="modules")

def create_app():
    app = Flask(__name__)
    registry.init_app(app)  # boots and registers Blueprints
    return app

Module routes.py:

# modules/Blog/routes.py
from flask import Blueprint, jsonify

blueprint = Blueprint("blog", __name__, url_prefix="/blog")

@blueprint.get("/")
def index():
    return jsonify([])

FastAPI Integration

from fastapi import FastAPI
from pymodules.integrations.fastapi import FastAPIModuleRegistry

app = FastAPI()
registry = FastAPIModuleRegistry(modules_path="modules", app=app)
registry.boot()  # auto-includes all enabled module APIRouters

App factory pattern:

registry = FastAPIModuleRegistry(modules_path="modules")

def create_app():
    app = FastAPI()
    registry.init_app(app)
    return app

Module routes.py:

# modules/Blog/routes.py
from fastapi import APIRouter

router = APIRouter(prefix="/blog", tags=["blog"])

@router.get("/")
def index():
    return {"message": "Blog index"}

Pure Python Usage

No framework required. Use ModuleRegistry directly:

from pymodules import ModuleRegistry

registry = ModuleRegistry(modules_path="modules")
registry.boot()

# Find a module
blog = registry.find("Blog")
print(blog.name, blog.version, blog.is_enabled)

# Enable / disable (writes to module.json)
registry.enable("Shop")
registry.disable("Blog")

# Iterate
for module in registry:
    print(module)

# Check existence
if "Blog" in registry:
    print("Blog module is registered")

# Boot hooks — run for every enabled module at boot time
@registry.on_boot
def on_module_boot(module):
    print(f"Booted: {module.name}")

# Import a submodule
models = blog.import_submodule("models.post")

Project Configuration — pymodules.toml

pymodules.toml lives at your project root and is committed to version control. It means you and your team never have to pass --modules-path manually.

# pymodules.toml
[pymodules]

# Where your modules/plugins live.
# Can be any name: "modules", "plugins", "apps", "src/features", etc.
modules_path = "modules"

# Default scaffold preset for `pymodules make` and `manage.py module_make`.
# Auto-detected from your environment — change if detection is wrong.
# Choices: default | plain | django | fastapi | flask
default_preset = "django"

# Informational — set by `pymodules init`, not read by the tool.
detected_framework = "django"

Config resolution order (highest priority first):

  1. --modules-path / --preset CLI flags
  2. pymodules.toml values
  3. Auto-detected framework
  4. Hardcoded fallbacks ("modules" / "default")

API Reference

ModuleRegistry

from pymodules import ModuleRegistry

registry = ModuleRegistry(
    modules_path="modules",   # str or Path
    scan_on_init=True,        # auto-scan on construction
)
Method / Property Returns Description
registry.all() list[Module] All modules (enabled + disabled)
registry.all_enabled() list[Module] Only enabled modules
registry.all_disabled() list[Module] Only disabled modules
registry.find(name) Module Find by name, raises ModuleNotFoundError
registry.exists(name) bool Check if a module exists
registry.count() int Total number of modules
registry.enable(name) None Enable a module (persists to disk)
registry.disable(name) None Disable a module (persists to disk)
registry.boot() None Boot all enabled modules
registry.scan() None Re-scan the modules directory
registry.module_path(name, *parts) Path Resolve path within a module
registry.on_boot(fn) fn Register a boot hook (decorator)
"Blog" in registry bool Membership test
for m in registry Iteration

Module

Property / Method Returns Description
module.name str Module name
module.path Path Absolute path to module directory
module.import_path str Dotted Python import path
module.is_enabled bool Current enabled state
module.enable() None Enable (writes module.json)
module.disable() None Disable (writes module.json)
module.version str Version from manifest
module.description str Description from manifest
module.author str Author from manifest
module.providers list[str] Provider dotted paths
module.manifest dict Raw manifest dict
module.has_file(*parts) bool Check if file exists inside module
module.import_submodule(path) module Import a submodule by relative dotted path

DjangoModuleRegistry

Extends ModuleRegistry with:

Method Returns Description
registry.installed_apps() list[str] Dotted app paths for INSTALLED_APPS
registry.url_patterns() list URL patterns from all module routes.py files
registry.migration_modules() dict[str, str] Dict for MIGRATION_MODULES setting
registry.collect_settings() dict Merged dict of all module-level settings

detect_framework()

from pymodules import detect_framework

info = detect_framework(search_path=None)  # defaults to cwd
# info.name          → "django" | "fastapi" | "flask" | "unknown"
# info.preset        → "django" | "fastapi" | "flask" | "default"
# info.confidence    → "high" | "medium" | "low"
# info.reason        → str
# info.all_detected  → list[str]

Exceptions

from pymodules import (
    PyModulesError,          # base exception
    ModuleNotFoundError,     # registry.find("Nonexistent")
    ModuleAlreadyExistsError,# generator.generate("Blog") when it exists + no force
    ModuleDisabledError,     # raised when accessing a disabled module's services
)

Feature Status

✅ Done — v0.1.0

Core

  • ModuleRegistry — scan, boot, find, enable, disable, iterate
  • Module — manifest, enable/disable state, import helpers, path helpers
  • module.json manifest with versioning, providers, publishes
  • ServiceProvider base class with register() and boot() hooks
  • Boot hooks via registry.on_boot(fn) decorator
  • Custom folder naming — modules/, plugins/, apps/, src/features/ — anything
  • Typed exceptions (ModuleNotFoundError, ModuleAlreadyExistsError)

Framework Detection

  • Three-layer detection: venv import → file fingerprints → dependency scanning
  • DJANGO_SETTINGS_MODULE env var detection
  • Priority resolution when multiple frameworks present (Django > FastAPI > Flask)
  • detect_framework() public API
  • pymodules detect CLI command with confidence display

CLI (pymodules)

  • pymodules init — project setup with auto-detection
  • pymodules make <n> — scaffold with preset auto-detection
  • pymodules list — table view with enable/disable filter
  • pymodules enable / pymodules disable
  • pymodules show — full module details
  • pymodules delete — with confirmation prompt
  • pymodules publish — asset/config publishing with groups
  • pymodules detect — framework detection report
  • pymodules presets — list all presets
  • --modules-path global flag + PYMODULES_PATH env var
  • pymodules.toml project config with walk-up discovery

Scaffold Presets

  • plain preset — module.json + __init__.py
  • default preset — framework-agnostic with providers, config, tests
  • django preset — models, views, apps.py, admin, serializers, migrations
  • django-api preset — DRF serializer/viewset/router scaffolding
  • fastapi preset — APIRouter, Pydantic schemas, service layer
  • fastapi-crud preset — full CRUD APIRouter with update/delete skeletons
  • flask preset — Blueprint, service layer
  • flask-api preset — full JSON CRUD Blueprint scaffolding
  • {folder} token in stubs — providers path always correct regardless of folder name

Django Integration

  • DjangoModuleRegistryinstalled_apps(), url_patterns(), api_url_patterns(), migration_modules(), collect_settings(), collect_policies()
  • AppConfig auto-detection from apps.py
  • Module-level URL routing with custom prefix
  • Module-level migrations via MIGRATION_MODULES
  • Module-level settings merged with collect_settings()
  • DRF access policy discovery from module policies.py / policies/
  • Dependency-aware startup ordering with warning fallback during Django settings evaluation
  • "pymodules" as Django app for manage.py command discovery
  • manage.py module_make — with --preset, --force, smart default to django
  • manage.py module_list — with --enabled / --disabled
  • manage.py module_enable / module_disable
  • manage.py module_show
  • manage.py module_delete — with --yes flag
  • manage.py module_publish — with --group, --force
  • manage.py module_make_model — lightweight per-file model scaffold
  • manage.py module_make_serializer — DRF serializer scaffolding
  • manage.py module_make_viewset — DRF ViewSet scaffolding
  • manage.py module_make_api_urls — DRF router URL scaffolding
  • manage.py module_make_model_migration — migration generation scoped to one model
  • manage.py module_make_migration — module-scoped makemigrations
  • manage.py module_migrate — module-scoped migrate

Dependencies & Boot Order

  • module.json requires declarations
  • Topological boot ordering for enabled modules
  • Circular dependency detection
  • Missing / disabled dependency errors

Flask Integration

  • FlaskModuleRegistry — auto-registers Blueprints
  • App factory pattern via init_app(app)
  • Multiple blueprints per module via blueprints list

FastAPI Integration

  • FastAPIModuleRegistry — auto-includes APIRouters
  • App factory pattern via init_app(app)
  • Multiple routers per module via routers list

Tests

  • Core registry tests (scan, find, enable, disable, iterate, exists)
  • Module tests (manifest, import path, enable persistence, has_file)
  • Generator tests (scaffold creation, preset selection, force, duplicate detection)
  • Detector tests (per-framework, confidence levels, priority, env var, dep scanning)

🔄 In Progress

  • pymodules upgrade command — diff existing module against current preset stubs and offer selective updates
  • Test coverage enforcementpytest-cov integration with minimum threshold config in pymodules.toml
  • Detector: setup.py scanning — legacy projects that don't use pyproject.toml or requirements.txt

Roadmap to Industry Standard

The following is what separates a useful personal tool from a package the Python community adopts as a standard.

Documentation & Developer Experience

  • Dedicated documentation site (MkDocs + Material theme) — full tutorials, how-to guides, API reference, migration guides
  • Interactive quickstartpymodules init walks the user through setup with prompts rather than silent file creation
  • VS Code extension — syntax highlighting for module.json, module tree sidebar, right-click "Create module" in explorer
  • PyCharm / IntelliJ plugin — same as above for JetBrains IDEs
  • Error messages with fix suggestions — every raised exception includes the exact command or code to resolve it

Architecture & Core

  • Module versioning and constraintsrequires: [{"name": "Auth", "version": ">=1.2.0"}]
  • Async boot supportasync def register() / async def boot() in service providers for async frameworks (FastAPI, Starlette, Litestar)
  • Module events / hooks system — typed inter-module communication without direct imports (module.emit("user.created", payload))
  • Lazy loading — modules only fully imported when first accessed, not at boot time
  • Hot reload — detect module.json changes at runtime and re-enable/disable without restart (development mode)

CLI & Tooling

  • pymodules upgrade — compare existing module files against current preset stubs, show diff, offer selective updates
  • pymodules lint — validate all module.json files, check for broken provider paths, missing __init__.py, import errors
  • pymodules graph — render module dependency graph in the terminal (or export to Mermaid/DOT)
  • pymodules test <ModuleName> — run tests scoped to a single module
  • pymodules stats — line counts, test coverage, complexity per module

Framework Support

  • Litestar integration — routers, dependency injection, middleware
  • Starlette integration — Mount-based routing
  • Celery integration — auto-discover tasks from enabled modules
  • SQLAlchemy integration — auto-collect models and migrations (Alembic)
  • Django REST Framework — auto-register routers from module api.py
  • Django REST Framework access policies — auto-discover AccessPolicy classes from module policies.py
  • Django Channels — WebSocket routing from module consumers.py

Testing

  • Integration tests — full Django, FastAPI, Flask app boot tests
  • Snapshot tests — assert generated scaffold files match expected output exactly
  • Matrix CI — test Python 3.10, 3.11, 3.12, 3.13 × Django 4.x/5.x × FastAPI × Flask
  • Mutation testingmutmut to verify test suite quality
  • 100% line coverage on core (module.py, registry.py, generator.py, detector.py)

Distribution & Community

  • PyPI release pipeline — build + twine check + GitHub Actions publish workflow
  • GitHub Actions CI/CD — test matrix plus release publish workflow
  • ChangelogCHANGELOG.md following Keep a Changelog format
  • Contributing guide — how to add presets, integrations, CLI commands
  • Custom preset support — let users define their own presets in pymodules.toml pointing to a local stubs directory
  • Plugin API — third-party packages can register new presets, CLI commands, and integrations via entry points
  • Security policySECURITY.md, responsible disclosure process
  • Benchmarks — boot time with N modules, import overhead vs flat structure

Production Readiness

  • pymodules.lock — deterministic module state snapshot (which modules are enabled, at what version) for reproducible deployments
  • Module health checksregistry.health() verifies all enabled modules can import cleanly without actually booting them
  • Structured logging — opt-in verbose boot logging with module name, provider, timing
  • Type stubspy.typed marker, complete __init__.pyi for IDE autocomplete on all public APIs
  • Thread safety — registry operations safe under concurrent access (WSGI multi-thread, Gunicorn workers)

Contributing

Contributions are welcome. Please open an issue first to discuss what you would like to change.

git clone https://github.com/tomcroot/pymodules
cd pymodules
pip install -e ".[dev]"
pytest

License

MIT — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytmodules-0.1.2.tar.gz (63.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytmodules-0.1.2-py3-none-any.whl (58.8 kB view details)

Uploaded Python 3

File details

Details for the file pytmodules-0.1.2.tar.gz.

File metadata

  • Download URL: pytmodules-0.1.2.tar.gz
  • Upload date:
  • Size: 63.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pytmodules-0.1.2.tar.gz
Algorithm Hash digest
SHA256 0eabb11de63520ac5d1098334fdeff28133464a50b446ae372bc0892ff02363f
MD5 21a54a7f31d9c963b4d3e7f9c9846ca2
BLAKE2b-256 74e80297561e1c9f0777af77e46fb1afe0a793b14601749e3b4232dba49b6a71

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytmodules-0.1.2.tar.gz:

Publisher: publish.yml on tomcroot/pymodules

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytmodules-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: pytmodules-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 58.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pytmodules-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1d4e1a8bbc926e0714e1416a503a9c0fbe7d44b743cc125b99a3c6fcd077b69c
MD5 f98f8e60844fe1b18cb8a231a7fb07d9
BLAKE2b-256 fc4e635f485fe7707a800560d7e1f002d284869719a3c448cb76db68eb8562f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytmodules-0.1.2-py3-none-any.whl:

Publisher: publish.yml on tomcroot/pymodules

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page