@Module¶
Spec
Full specification: specs/module.md
· Item: AJ-8
Purpose¶
@Module(...) is the class decorator that groups a set of providers,
imports other modules, declares which providers leak to importers via
exports=, and serves as the root of the DI graph that
AjolopyFactory.create walks at boot.
The decorator is metadata-only at definition time: it stamps a frozen
_ajolopy_module dataclass on the class and returns the class
unchanged. compile_module(root) is the pure, testable function that
walks the import graph, validates visibility, and populates a
Container.
Reach for @Module as soon as the app outgrows a single file —
NestJS-style composition for the dependency graph.
Signature¶
def Module(
*,
imports: list[ModuleClass | ForwardRef] | None = None,
providers: list[type] | None = None,
controllers: list[type] | None = None,
agents: list[type] | None = None,
workflows: list[type] | None = None,
evals: list[type] | None = None,
exports: list[type] | None = None,
global_: bool = False,
) -> Callable[[type], type]: ...
Quick example¶
from ajolopy import Module, compile_module, forwardRef
@Module(global_=True, providers=[], exports=[])
class ConfigModule: ...
@Module(
providers=[],
controllers=[],
exports=[],
)
class UsersModule: ...
@Module(
imports=[
ConfigModule,
UsersModule,
forwardRef(lambda: BillingModule), # break a circular import
],
)
class AppModule: ...
@Module(imports=[UsersModule])
class BillingModule: ...
compiled = compile_module(AppModule)
# compiled.container — populated Container ready to resolve()
# compiled.controllers / .agents / .workflows / .evals — flat lists
# compiled.module_order — modules in compile order
Kwargs¶
| Kwarg | Type | Default | Description |
|---|---|---|---|
imports |
list[ModuleClass \| ForwardRef] \| None |
None |
Other @Module classes to compose. forwardRef(lambda: ...) breaks circular imports. |
providers |
list[type] \| None |
None |
Classes owned by this module. Registered into the container with the scope from @Injectable. |
controllers |
list[type] \| None |
None |
@Controller classes mounted via this module. |
agents |
list[type] \| None |
None |
@Agent classes owned by this module. |
workflows |
list[type] \| None |
None |
@Workflow classes owned by this module. |
evals |
list[type] \| None |
None |
@Eval suites owned by this module. |
exports |
list[type] \| None |
None |
Providers visible to importers. Must be a subset of providers=. |
global_ |
bool |
False |
When True, the module's exports= become resolvable from every other module without an explicit imports= entry. |
Escape hatches¶
global_=Truefor cross-cutting modules (ConfigModule,LoggerModule) that every other module wants without re-importing.forwardRef(lambda: OtherModule)for circular imports — resolves once duringcompile_module.- Custom
Container. Passcompile_module(root, container=...)to pre-seed providers (test mocks, etc.) into a known container.
Common gotchas¶
- A class becomes a module only via the
@Module(...)decorator.compile_module(NotAModule)raisesNotAModuleError. There is no auto-discovery. exports=must be a subset ofproviders=. Re-exporting an imported module's surface is not supported in v0.1 — import the upstream module directly instead.- Two modules declaring the same provider in the same graph raise
DuplicateProviderErrorat compile time. Only one module owns each token; other modules import the owner. _ajolopy_moduleis not inherited by subclasses. Re-decorate the subclass if you want it to count as a module.agents=/workflows=/evals=acceptlist[type]and register the classes as singletons. The downstream items that mount them (factory, controllers, etc.) iterate these lists;@Moduleitself does no mount logic.
See also¶
@Injectable— declare per-class scope.@Controller— HTTP entry points registered viacontrollers=.- Spec:
specs/module.md.