Skip to content

Discovery & entry points ​

Modules are Python packages registered via the simple_module entry-point group. There is no module registry file, no INSTALLED_MODULES list in settings — if the package is installed in the environment and declares the entry point, it's a module.

Declaring a module ​

Every module's pyproject.toml ends with:

toml
[project.entry-points.simple_module]
orders = "orders.module:OrdersModule"
  • The key (orders) is the entry-point name. It's used for reporting only — the framework authoritative name is ModuleMeta.name.
  • The value is <import_path>:<class_name>. The class must inherit from simple_module_core.module.ModuleBase and declare a non-null meta = ModuleMeta(...) class attribute.

Entry points are registered at install time, not at import time. After editing a module's pyproject.toml, re-run uv sync --all-packages (or make install) so the installer writes the new metadata to the venv's *.dist-info/entry_points.txt.

Discovery algorithm ​

simple_module_core.discovery.discover_modules():

  1. Iterates importlib.metadata.entry_points(group="simple_module").
  2. For each entry point, calls .load() to import the module's module.py and pull out the class, then instantiates it (module_cls()).
  3. Validates: the loaded object must be a ModuleBase subclass with a meta attribute of type ModuleMeta.
  4. If SM_MODULES_ENABLED is set, filters to only the modules whose ModuleMeta.name (case-insensitive) appears in the list.
  5. Returns a list of ModuleBase instances (one per module, constructed with no args). A separate topological_sort() call (made by create_app) then orders them by ModuleMeta.depends_on using a stable tiebreaker (name).

Failures:

  • Import error (e.g. syntax error in module.py) — lenient mode: logged + skipped. Strict mode: raises InvalidModuleError.
  • Missing meta — emits SM001. Strict mode: raises.
  • Duplicate meta.name — emits SM008 (error). Always fails boot, even in dev, because names are used as table-name prefixes.
  • Cycle in depends_on — raises CircularDependencyError. Always fails boot.
  • Missing depends_on target (a module that is not installed, not enabled, or dropped because another auth provider was selected) — outside development, topological_sort(strict=True) raises one MissingDependencyError (an InvalidModuleError) naming every (module, dependency) pair, and the boot fails. In development it is logged per pair and reported as SM030 (a warning at boot, an error in make doctor). The outbox worker sorts leniently. GH #501.

ModuleMeta ​

python
from simple_module_core.module import ModuleMeta

meta = ModuleMeta(
    name="Orders",  # PascalCase, globally unique
    route_prefix="/api/orders",  # where the API router mounts
    view_prefix="/orders",  # where the view router mounts
    depends_on=["Products"],  # required, and sorted after
    optional_depends_on=["Settings"],  # sorted after when loaded; may be absent
    version="1.0.0",  # semver for the module
    i18n_audience="public",  # who gets this module's locale catalog
)

name ​

The authoritative identifier. It's used for:

  • Table-name prefix (orders_*) — every module's tables share the host's single schema on both Postgres and SQLite, so the prefix is what keeps them collision-free.
  • Inertia page namespace ("Orders/Browse").
  • Diagnostic reporter name.
  • Feature-flag and permission grouping.

Rule of thumb: use PascalCase and keep it stable. Renaming a module after release requires a data migration and a coordinated client-side rename.

depends_on ​

A list of other modules' meta.name values that must be loaded first. The framework topo-sorts on this before invoking lifecycle hooks. Use this when:

  • Your module's middleware must sit inside/outside another module's middleware. See Middleware pipeline.
  • Your module's register_routes reads another module's service to compose endpoints.
  • Your module subscribes to events published by another module.

depends_on is not a dependency-injection mechanism. It controls order and presence — you still import the services you need. Outside development a missing entry refuses the boot (see Failures above), so list only modules yours cannot work without.

optional_depends_on ​

Modules to sort after when they are loaded, for an ordering-only relationship: your middleware wraps outside theirs, or you subscribe to an event they publish, but your module works without them. Present modules order exactly as depends_on would, so moving an entry here never changes the boot order; an absent one is fine in every environment. The modules that sorted after Settings only for ordering (users, tenants, file_storage, background_tasks, site_lock, api_tokens, keycloak) declare it here, because the settings mechanism lives in the framework (#500) and the settings plugin may be disabled.

version ​

Semver string. Used in diagnostic/boot logging to help operators correlate deployed module versions with bug reports. Unless a module declares requires_framework, it is not parsed for automated version-range checks.

requires_framework ​

Optional PEP 440 specifier (e.g. ">=1.0,<2.0") for the framework API version the module supports. When set, the module is rejected at boot if the installed simple_module_core.FRAMEWORK_API_VERSION doesn't satisfy it. When None (the default), no compatibility check runs. See module authoring.

i18n_audience ​

Who the module's locale catalog is shipped to — "public" (the default) or "admin".

Catalogs ride the Inertia shared props on full page loads, so a module whose UI sits entirely behind login can declare i18n_audience="admin" and stop anonymous visitors downloading admin form labels on every public page. The catalog is shipped as soon as the user authenticates, and server-side Translator lookups always see every namespace either way. See Internationalization → Audience.

The simple_module group ​

Entry points are the same mechanism that ships with importlib.metadata:

text
.venv/lib/python3.12/site-packages/orders-1.0.0.dist-info/entry_points.txt:

    [simple_module]
    orders = orders.module:OrdersModule

You can inspect what the framework sees with:

bash
uv run python -c "
from importlib.metadata import entry_points
for ep in entry_points(group='simple_module'):
    print(ep.name, '->', ep.value)
"

Disabling modules at runtime ​

Set SM_MODULES_ENABLED to a comma-separated allow-list of module names (matched case-insensitively against ModuleMeta.name). Useful for:

  • Running tests against a minimal world (SM_MODULES_ENABLED=Users,Orders).
  • Temporarily disabling a misbehaving module in production without a redeploy.
  • Sharding a large app into multiple deployments (e.g. background-worker process loads only the background_tasks module).

Disabling via env doesn't remove the module's database tables. Re-enabling picks up where it left off.

Strict-mode checklist ​

Before flipping to SM_ENVIRONMENT=production, verify:

  • [ ] Every module's pyproject.toml declares the simple_module entry point.
  • [ ] Every ModuleBase subclass has meta = ModuleMeta(...).
  • [ ] App boot in development reports no diagnostic errors (SM001, SM008, SM009, SM010, SM016 are all ERROR-level — production boot will fail on any of them).
  • [ ] alembic upgrade head has been run against the production DB.
  • [ ] SM_SECRET_KEY is not change-me-in-production.

Failure on any of the above produces a clean boot-time exception rather than a silent misbehavior.

Released under the MIT License.