Skip to main content

Spring-inspired CDI (Context and Dependency Injection) for Python

Project description

alt-python-cdi

Language Python License: MIT

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 (OrderServiceorder_service). CamelCase names passed to Singleton({"name": "myBean"}) are also converted (myBeanmy_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:

  1. Instantiates all Singleton components.
  2. Injects the ApplicationContext by calling set_application_context(ctx) on any bean that defines it.
  3. Autowires None-valued constructor attributes by name.
  4. Resolves Property placeholders (e.g. '${app.port:8080}') from config.
  5. 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() and destroy() must be regular def methods, not async def. If you need to call async code, bridge with asyncio.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-Context form (ApplicationContext(Context([...]))) creates an empty EphemeralConfig internally — 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


Download files

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

Source Distribution

alt_python_cdi-1.1.1.tar.gz (16.4 kB view details)

Uploaded Source

Built Distribution

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

alt_python_cdi-1.1.1-py3-none-any.whl (14.0 kB view details)

Uploaded Python 3

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

Hashes for alt_python_cdi-1.1.1.tar.gz
Algorithm Hash digest
SHA256 3c61294a3b964fc642e4c30e8c519e483c50a30ebe7535423c72af052e10a708
MD5 ab44d5519c8113ff176e2a95d400af0f
BLAKE2b-256 fbfb04f18afbaa9b265bcc77bb8ffb0baf3843b7be9e3914f05d835268bf5b9d

See more details on using hashes here.

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

Hashes for alt_python_cdi-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8c8ae0b48c166aa1fa5c687374395b534d5189a24d83a1ce48ea8f1379fcaf3b
MD5 3739b6f9c0726188488f72f3dbd03441
BLAKE2b-256 6b4da35c23093ffa933b0f20df1a97f8b2e30308beaffca98a9557bcf745663e

See more details on using hashes here.

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