Spring-inspired CDI (Context and Dependency Injection) for Python
Project description
alt-python-cdi
IoC container and dependency injection for the alt-python framework. Provides
ApplicationContext, Singleton, Prototype, Context, Component,
Property, and Scopes — a synchronous, profile-aware CDI container with
name-based autowiring and lifecycle management.
The design is a direct port of the Spring Framework's
ApplicationContext and component model to idiomatic Python.
Part of the alt-python/boot monorepo.
Install
uv add alt-python-cdi # or: pip install alt-python-cdi
Requires Python 3.12+, alt-python-config, and alt-python-logger.
Quick Start
from config import EphemeralConfig
from cdi import ApplicationContext, Context, Singleton
class UserRepository:
def __init__(self):
self._users = []
def add(self, user):
self._users.append(user)
def find_all(self):
return list(self._users)
class UserService:
def __init__(self):
self.user_repository = None # CDI-autowired by name
def create_user(self, name):
self.user_repository.add({"name": name})
cfg = EphemeralConfig({"logging": {"level": {"/": "warn"}}})
ctx = ApplicationContext({
"config": cfg,
"contexts": [Context([Singleton(UserRepository), Singleton(UserService)])],
})
ctx.start()
ctx.get('user_service').create_user("Alice")
print(ctx.get('user_repository').find_all()) # [{'name': 'Alice'}]
Autowiring
CDI wires beans by name matching. Set a constructor attribute to None
and name it after the target bean (in snake_case) — CDI sets it to the live
instance after all singletons are instantiated.
class OrderService:
def __init__(self):
self.order_repository = None # wired to the OrderRepository bean
self.email_service = None # wired to the EmailService bean
ApplicationContext converts class names to snake_case for the registry key
(OrderService → order_service). CamelCase names passed to Singleton({"name": "myBean"}) are also converted (myBean → my_bean). Use ctx.get('order_service')
to retrieve beans.
Lifecycle
The CDI lifecycle mirrors Spring's component lifecycle:
| Phase | Spring | CDI Python |
|---|---|---|
| Wire + init | refresh() |
ctx.start() |
| Post-construct | @PostConstruct |
bean.init() |
| Pre-destroy | @PreDestroy |
bean.destroy() |
| Context wiring callback | ApplicationContextAware |
bean.set_application_context(ctx) |
After ctx.start(), CDI:
- Instantiates all
Singletoncomponents. - Injects the
ApplicationContextby callingset_application_context(ctx)on any bean that defines it. - Autowires
None-valued constructor attributes by name. - Resolves
Propertyplaceholders (e.g.'${app.port:8080}') from config. - Calls
init()on each bean that defines it, in dependency order.
On shutdown (SIGINT / explicit stop), CDI calls destroy() on each bean in
reverse order.
init()anddestroy()must be regulardefmethods, notasync def. If you need to call async code, bridge withasyncio.run(). See ADR-012.
Component Definitions
Singleton(reference_or_dict)
Registers a class as a CDI-managed singleton. The same instance is returned on
every ctx.get() call.
# Class form — name derived from class name (snake_case)
Singleton(OrderService)
# Dict form — explicit name, conditions, scope
Singleton({
"reference": OrderService,
"name": "orderService",
"scope": Scopes.SINGLETON,
})
Dict form keys:
| Key | Type | Description |
|---|---|---|
reference |
class | The class to instantiate |
name |
str |
CDI bean name (camelCase converted to snake_case) |
scope |
str |
Scopes.SINGLETON (default) or Scopes.PROTOTYPE |
primary |
bool |
Wins disambiguation when multiple beans share a name |
Prototype(reference_or_dict)
Like Singleton but creates a new instance on every ctx.get() call. Useful
for stateful per-request objects.
Context([components])
Groups component definitions. An ApplicationContext accepts one or more
Context objects.
from cdi import Context, Singleton
repo_context = Context([Singleton(UserRepository)])
svc_context = Context([Singleton(UserService)])
app_ctx = ApplicationContext({
"config": cfg,
"contexts": [repo_context, svc_context],
})
Property
Declares a config-value property. Use placeholder syntax '${path:default}' as
the default value in __init__:
class ServerConfig:
def __init__(self):
self.port = '${server.port:8080}'
self.host = '${server.host:localhost}'
self.timeout = '${server.timeout:30}'
CDI resolves placeholders against the wired config bean before calling init().
Profiles
Restrict a bean to specific active profiles using the profiles class attribute:
class DevEmailService:
profiles = ['dev']
class ProdEmailService:
profiles = ['prod']
When PY_ACTIVE_PROFILES=dev, only DevEmailService is instantiated. Beans
without a profiles attribute are always active.
Use primary = True on the profile-conditional bean to ensure it wins when two
beans would resolve to the same attribute name:
class DevEmailService:
profiles = ['dev']
primary = True
Scopes
from cdi import Scopes
Scopes.SINGLETON # "singleton" — one instance per context
Scopes.PROTOTYPE # "prototype" — new instance per ctx.get() call
Dependency Ordering
Set depends_on as a class attribute to declare explicit ordering:
class SchemaInitializer:
depends_on = ['data_source']
def init(self):
# data_source is guaranteed to be initialised before this runs
...
CDI resolves depends_on chains before calling init(), even if the dependency
is not directly wired via a None attribute.
ApplicationContext API
ApplicationContext(options)
ctx = ApplicationContext({
"config": cfg, # config-like object (required)
"contexts": [context] # list of Context objects (required)
})
Use the dict form. The single-
Contextform (ApplicationContext(Context([...]))) creates an emptyEphemeralConfiginternally — it does not load any config files from disk. See the monorepo README for the canonical invocation pattern.
ctx.start()
Wires and initialises all components. Equivalent to Spring's
ApplicationContext.refresh() + start().
ctx.get(name)
Retrieve a bean by name (snake_case). Raises KeyError if not found.
svc = ctx.get('order_service')
ctx.stop()
Calls destroy() on all beans in reverse init order.
Using with Boot
The canonical entry point is Boot.boot(), which handles config loading, banner
printing, and CDI wiring in one call:
from boot import Boot
from cdi import Context, Singleton
Boot.boot({
'contexts': [Context([Singleton(MyService), Singleton(Application)])]
})
Boot.boot() auto-registers config, logger_factory, and
logger_category_cache as CDI beans before ctx.start() is called — any bean
with self.config = None receives the live config instance without extra wiring.
For tests, use Boot.test():
from boot import Boot
from cdi import Context, Singleton
ctx = Boot.test({'contexts': [Context([Singleton(MyService)])]})
svc = ctx.get('my_service')
All Exports
from cdi import (
ApplicationContext,
Component,
Context,
Property,
Prototype,
Scopes,
Singleton,
)
Spring Attribution
| Spring concept | alt-python-cdi equivalent |
|---|---|
@Component / @Service / @Repository |
Singleton |
@Autowired (field injection) |
self.dependency = None naming convention |
@Value("${key:default}") |
self.port = '${server.port:8080}' |
@PostConstruct |
def init(self) |
@PreDestroy |
def destroy(self) |
ApplicationContextAware |
def set_application_context(self, ctx) |
ApplicationContext.refresh() |
ctx.start() |
ApplicationContext.getBean() |
ctx.get('bean_name') |
@Profile |
profiles = ['dev'] class attribute |
@Primary |
primary = True class attribute |
@DependsOn |
depends_on = ['other_bean'] class attribute |
| Prototype scope | Prototype(MyClass) |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file alt_python_cdi-1.1.1.tar.gz.
File metadata
- Download URL: alt_python_cdi-1.1.1.tar.gz
- Upload date:
- Size: 16.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c61294a3b964fc642e4c30e8c519e483c50a30ebe7535423c72af052e10a708
|
|
| MD5 |
ab44d5519c8113ff176e2a95d400af0f
|
|
| BLAKE2b-256 |
fbfb04f18afbaa9b265bcc77bb8ffb0baf3843b7be9e3914f05d835268bf5b9d
|
File details
Details for the file alt_python_cdi-1.1.1-py3-none-any.whl.
File metadata
- Download URL: alt_python_cdi-1.1.1-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c8ae0b48c166aa1fa5c687374395b534d5189a24d83a1ce48ea8f1379fcaf3b
|
|
| MD5 |
3739b6f9c0726188488f72f3dbd03441
|
|
| BLAKE2b-256 |
6b4da35c23093ffa933b0f20df1a97f8b2e30308beaffca98a9557bcf745663e
|