Instruction file imported from orionis-framework/framework (
.github/instructions/di-container-providers.instructions.md). Copyright stays with the author.
Container, Providers and Facades
Read the module docs first
| Topic | Manual |
|---|---|
Container, Binding, Lifetime, ScopeManager, Facade/FacadeMeta |
orionis/container/docs/README.md (.es.md) |
Catalogue of the 16 facades + DateTime |
orionis/support/facades/docs/README.md (.es.md) |
| Reflection used to resolve dependencies | orionis/introspection/docs/README.md |
orionis/foundation/ has no docs/ yet: for the application lifecycle and
config entities, read the code. Everything below is rules and gotchas, not an
API reference.
Application lifecycle
Application(...)inbootstrap/app.py— singleton, extendsContainer.withRouting / withScheduler / withExceptionHandler / withProviders / withMiddleware— declarative config, frozen withFreezeThaw.freeze()on boot.app.create()— asserts Python version, loadsCORE_CONFIG+config/*.py, sets timezone/locale viaDateTime._loadConfig, self-registersIApplication, runsregister()of every provider, stores deferred ones in_deferred_providers.Application.__onStartup()— runsboot()of eager providers. Only fires under the HTTP or CLI runtime, never from a bareimport bootstrap.app.- Runtime:
handleASGI/handleRSGI(HTTP) orhandleCommand(CLI).
Container
- Singleton per subclass (
_instancesdict +threading.RLockdouble-check in__new__). - Lifetimes:
TRANSIENT = 1,SINGLETON = 2,SCOPED = 3. - Registration:
bind,singleton,scoped,transient,instance(contract, obj, alias=None). Aliases are strings and are global only. - Resolution is async:
make,build,invoke,call.make()uses the singleton cache;build()always creates a new instance. async with app.beginScope():forSCOPED; resolving scoped without a scope raisesRuntimeError.- Cycles are detected via a
ContextVarfrozenset stack →CircularDependencyException. - Creation of singletons, scoped services and deferred providers is guarded by
__creationLock(key)(oneasyncio.Lockper key, bound to the running loop) with double-checked locking and an anti-deadlock guard on the resolution stack. - A parameter annotated with a subclass of
orionis.schemas.schema.Schemamakes the container validate the request body automatically.
Hard DI rules
| Rule | Why |
|---|---|
Never from __future__ import annotations in a class the container builds |
Reflection treats string annotations as forward refs of type str and injects garbage |
Import constructor types at runtime (not under TYPE_CHECKING), with file-level # ruff: noqa: TC001 |
Same reason |
| A constructor parameter typed with an ABC needs an explicit binding registered first | Otherwise make() falls back to build() and raises TypeError: Argument 'concrete' must be a class type, got 'ABCMeta' instead. |
| Concrete classes auto-resolve without a binding | The container recursively resolves their constructor |
| Inside code built during startup, inject the contract, never a facade | Facades are only safe after the boot() that pins them |
Private methods that are not reflected by the container may annotate with
TYPE_CHECKING-only imports (PEP 649 makes annotations lazy).
Service providers
from orionis.container.providers import ServiceProvider
class AppServiceProvider(ServiceProvider):
def register(self) -> None:
"""Register application services."""
self.app.singleton(IMyService, MyService)
async def boot(self) -> None:
"""Bootstrap application services."""
await MyFacade.pin()
register()is sync and only declares bindings;boot()is async and does the wiring.DeferrableProvider.provides()only declares keys; the real deferred registry is built byApplication.create().- The 15 core providers live in
orionis/foundation/core_providers.py:CacheProvider, CatchProvider, ConnectionManagerProvider, EncrypterProvider, HashProvider, LocalizationProvider, LoggerProvider, QueryBuilderProvider, ReactorProvider, RouterProvider, ScheduleProvider, SchemaProvider, StorageProvider, TestingProvider, ViewServiceProvider. - If a facade is consumed without
awaitfrom synchronous framework code, its provider must NOT be deferrable — it has to be eager so the facade is pinned at startup (this is whyHashProviderandEncrypterProviderare eager).
Facades
orionis/support/facades/ — each facade only overrides getFacadeAccessor(); the
parallel .pyi exists solely for editor autocompletion and is never executed.
16 facades: Application, Cache, Catch, Crypt, DB, Hash, Lang, Log, Reactor, Route, Schedule, Schema, Session, Storage, Test, View — plus DateTime, which is not a
facade but a classmethod-only wrapper over pendulum and the single source of truth for
timezone/locale.
# Unpinned: every attribute access returns a _FacadeDispatch
repo = Cache.store("redis") # _FacadeDispatch, NOT the repository
repo = await Cache.store("redis") # correct without pin (and pins as a side effect)
# Pinned (after await Facade.pin() or the provider boot): direct passthrough
Hash.make("secret") # synchronous, no await
_FacadeDispatchimplements__await__,__aenter__and__aexit__, soasync with Schema.create(...)requires a pin butawait Schema.create(...)does not.- A real classmethod defined on the facade subclass (e.g.
DB.table()) bypassesFacadeMeta.__getattr__entirely — that hook is only an attribute-not-found fallback. Do not delete such an override without grepping for callers that rely on the unpinned path. _pinned_instanceis a class attribute shared process-wide.Sessionis pinned and unpinned per request insideStartSessionMiddleware; concurrent code must readrequest.state.sessioninstead.
Configuration
- Config entities:
orionis/foundation/config/<section>/entities/*.py, dataclassesfrozen=True, kw_only=TrueextendingBaseEntity, validated in__post_init__, reading env viadefault_factory=lambda: Env.get("VAR", default). CORE_CONFIGhas 14 sections:app, auth, cache, database, filesystems, hashing, http, logging, mail, queue, scheduler, session, testing, view.- The application overrides them in
config/*.pywithBootstrap*classes. app.config("path.to.key")returnsNonefor unknown keys — always provide a fallback in the consumer.
Adding or removing a provider, a core command or a config entity field invalidates nothing automatically: delete
storage/framework/bootstrap(or runreactor optimize:clear) or the app keeps booting with stale compiled metadata.