Skip to main content

Lightweight, thread-safe dependency injection container for Python

Project description

Overview

PyPI Python License

PS DI is a lightweight, thread-safe dependency injection container for Python. It provides a DI class that manages service registration, resolution, and automatic constructor injection. Registrations support singleton and transient lifetimes, priority-based ordering, and resolution by type or string name.

For working project examples, see the ps-poetry-examples repository.

Installation

pip install ps-dependency-injection

Or with Poetry:

poetry add ps-dependency-injection

Quick Start

from ps.di import DI, Lifetime

di = DI()

di.register(Logger).factory(Logger, "app")
di.register(UserRepository, Lifetime.TRANSIENT).implementation(UserRepository)

repo = di.resolve(UserRepository)

View full example

Register Services

The register method accepts a type (or string key), an optional Lifetime, and an optional Priority. It returns a Binding object that configures how the service is created.

  • .factory(callable, *args, **kwargs) — Registers a callable that produces the service. Typed parameters not covered by explicit arguments are resolved from the container at registration time, using the same injection rules as satisfy. Explicit positional and keyword arguments take precedence over container resolution.
  • .implementation(cls) — Registers a class whose constructor is invoked via spawn, allowing the container to inject known dependencies automatically.
from ps.di import DI, Lifetime

di = DI()

# Singleton (default) — one shared instance
di.register(Logger).factory(Logger, "app")

# Transient — new instance on every resolve
di.register(UserRepository, Lifetime.TRANSIENT).implementation(UserRepository)

Lifetime values:

  • SINGLETON — The factory is called once; all subsequent resolves return the same instance. This is the default.
  • TRANSIENT — The factory is called on every resolve, producing a new instance each time.

Resolve Services

Use resolve to retrieve the highest-priority registration for a type, or resolve_many to retrieve all registrations ordered by priority.

service = di.resolve(Logger)          # Logger | None
all_loggers = di.resolve_many(Logger) # list[Logger]

resolve returns None when no registration exists for the requested type. resolve_many returns an empty list in that case.

Both methods also accept a string type name instead of a class:

service = di.resolve("Logger")

String resolution matches against the __name__ attribute of registered types and raises ValueError when no match is found.

Priority

Each registration carries a Priority that determines its position relative to other registrations for the same type. Higher-priority registrations are resolved first by resolve and appear earlier in the list returned by resolve_many.

from ps.di import DI, Priority

di = DI()

di.register(NotificationService, priority=Priority.LOW).factory(NotificationService, "email")
di.register(NotificationService, priority=Priority.HIGH).factory(NotificationService, "sms")
di.register(NotificationService, priority=Priority.MEDIUM).factory(NotificationService, "push")

primary = di.resolve(NotificationService)  # sms (HIGH wins)

View full example

Priority values: LOW (default), MEDIUM, HIGH. When multiple registrations share the same priority, the most recently registered one wins.

Spawn Objects

spawn instantiates a class without registering it, injecting constructor dependencies from the container automatically. It inspects type hints on __init__ parameters and resolves them as follows:

  • A parameter typed as DI or a subclass of DI receives the container itself.
  • A parameter typed as List[T] receives the result of resolve_many(T).
  • A parameter typed as Optional[T] receives the result of resolve(T), falling back to the default value when nothing is registered.
  • Any other typed parameter receives the result of resolve(T). If resolve returns None and no default exists, spawn raises ValueError.
  • A parameter without a type annotation is resolved by name: the parameter name is normalized (case-folded with underscores removed) and matched against the __name__ of registered types using the same normalization. For example, a parameter named application matches a registered Application type, and event_dispatcher matches EventDispatcher.

Positional and keyword arguments passed to spawn override automatic resolution:

from ps.di import DI

di = DI()
di.register(Logger).factory(Logger, "app")

repo = di.spawn(UserRepository)                       # Logger injected from container
repo = di.spawn(UserRepository, logger=custom_logger)  # explicit override

Satisfy Functions

satisfy binds a callable to dependencies resolved from the container at the time of the call, returning a new callable that accepts any remaining parameters at invocation time.

  • Parameters with registered types are resolved from the container automatically.
  • Parameters with defaults fall back to their default values when no registration exists.
  • Parameters typed as List[T] receive all registered instances of T.
  • Parameters typed as Optional[T] receive None when no registration exists.
  • Parameters marked with REQUIRED are excluded from DI resolution and must be supplied by the caller.
  • Parameters without a type annotation are resolved by name using the same normalization rules as spawn (case-folded, underscores removed, matched against registered type names).
from ps.di import DI, REQUIRED

log_message = di.satisfy(format_log, message=REQUIRED)

print(log_message(message="Application started"))
print(log_message(message="Low disk space", level="WARNING"))

View full example

The returned callable accepts keyword arguments at invocation time. Any keyword argument passed at invocation time overrides the corresponding resolved value, including DI-resolved parameters.

Scopes

scope() creates a child DI instance that inherits all registrations from the parent but maintains its own isolated registry. This is useful for per-request, per-session, or any other short-lived context that needs additional or overriding registrations without affecting the parent container.

Resolution in a scoped container follows these rules:

  • resolve checks the scoped registry first; if nothing is registered, it falls through to the parent.
  • resolve_many returns scoped registrations followed by parent registrations, with scoped results first.
  • spawn and satisfy use the scoped resolver, so injected dependencies prefer scoped registrations.
  • Name-based resolution (for untyped parameters) searches the scoped registry first, then the parent.
  • A parameter typed as DI receives the scoped instance, not the parent.

Scopes support the context manager protocol. Exiting the with block clears the scoped registry and releases all singleton instances held by the scope, enabling deterministic cleanup of resources such as database connections or file handles.

with di.scope() as request_scope:
    request_scope.register(RequestContext).factory(RequestContext, request_id)
    handler = request_scope.spawn(RequestHandler)
    handler.handle()
# scoped singletons released here; parent container unaffected

View full example

The root container supports the same context manager protocol. Exiting a with di: block clears all registrations and releases every singleton instance held by the container.

Scopes can be nested arbitrarily. Each level sees its own registrations plus all ancestor registrations, with closer scopes taking precedence.

Thread Safety

All registration and resolution operations are protected by internal locks. Singleton creation uses double-checked locking so the factory is called exactly once even under concurrent access. Transient registrations produce independent instances per call with no shared mutable state.

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

ps_dependency_injection-0.2.25.tar.gz (6.5 kB view details)

Uploaded Source

Built Distribution

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

ps_dependency_injection-0.2.25-py3-none-any.whl (7.6 kB view details)

Uploaded Python 3

File details

Details for the file ps_dependency_injection-0.2.25.tar.gz.

File metadata

  • Download URL: ps_dependency_injection-0.2.25.tar.gz
  • Upload date:
  • Size: 6.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.2 CPython/3.13.12 Linux/6.17.0-1010-azure

File hashes

Hashes for ps_dependency_injection-0.2.25.tar.gz
Algorithm Hash digest
SHA256 c4704cb25dd21abd21a4db2bf7656200c6aa568e2f587d7156f23c33d7598b56
MD5 ebef694b816ef43e4b39ddab529b25d0
BLAKE2b-256 0028a531a6e534f68cfb3e27cd774bc94a4a3cfccf9bbe1877d597a90109d4d1

See more details on using hashes here.

File details

Details for the file ps_dependency_injection-0.2.25-py3-none-any.whl.

File metadata

File hashes

Hashes for ps_dependency_injection-0.2.25-py3-none-any.whl
Algorithm Hash digest
SHA256 3ee311c02467ad58169451dee7d2516010b8ff8bd597f6708dd2e93cac11454c
MD5 e0c5d54f9b7e2ed3bc566a96402eff91
BLAKE2b-256 5de9320fc7c766d7e3a6f802c1106c6dc8589a78fdf93b2a16c43a40b5f3fde2

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