Skip to main content

Static, lazily-resolved declarative DI containers built on dependency-injector.

Project description

static-dependency-injector

PyPI License GitHub

Static, lazily-resolved declarative DI containers built on top of dependency-injector.

Declare providers as annotated class attributes and read them back as resolved values straight from the container class — no instance, no () call. Overrides are fully type-checked: the provider names and value types are verified (and autocompleted) by ty, mypy and pyright.

from static_dependency_injector import static_providers as sp
from static_dependency_injector.containers import StaticDeclarativeContainer


class Services(StaticDeclarativeContainer):
    config: Config = sp.Singleton(load_config)
    db: Database = sp.Singleton(Database, config=config)


db = Services.db                                # resolved Database (lazy, cached)

with Services.set_overrides(db=fake_db):        # scoped — auto-restored on exit
    ...
Services.set_overrides(db=fake_db)              # or permanent

Services.set_overrides(dbb=fake_db)             # ❌ unknown provider (type error)
Services.set_overrides(db=123)                  # ❌ wrong value type (type error)

Providers stay lazy: the underlying callables are not invoked until first access, and each provider keeps dependency-injector's caching semantics.

Installation

pip install static-dependency-injector
# or
uv add static-dependency-injector

Why

  • Read providers as valuesServices.db returns the resolved instance, no () call and no container instance.
  • Fully typed — reads resolve to the dependency's type, and set_overrides checks provider names and value types under ty, mypy and pyright (Pylance), with autocompletion on the real fields.
  • Lazy & cached — keeps dependency-injector's resolution and per-provider caching semantics; nothing is built until first access.
  • Test-friendlyset_overrides (scoped context manager or permanent) for per-test overrides, plus TestContextSingleton auto-reset via a bundled pytest plugin.
  • Thin — a typed wrapper over dependency-injector; no vendoring, no magic beyond the container metaclass.

Providers

Typed wrappers over dependency-injector providers (static_providers):

Factory, Singleton, ThreadSafeSingleton, ThreadLocalSingleton, ContextLocalSingleton, Callable, Coroutine, Object, Resource, Dependency, Selector, Provider, Container (nested container), and the test-scoped TestContextSingleton.

Each is generic in its resolved type, so Services.db reads back as the dependency's type.

Nested containers

Nest a static container with Container; reads chain through it, and overriding the sub-container flows through:

class Inner(StaticDeclarativeContainer):
    db: Database = sp.Singleton(Database)

class Outer(StaticDeclarativeContainer):
    inner: type[Inner] = sp.Container(Inner)

db = Outer.inner.db                 # resolved Database (typed)
with Inner.set_overrides(db=fake):  # reflected through Outer.inner.db
    ...

Test-scoped providers

TestContextSingleton holds a value for the duration of one test and is reset between tests. Reset is driven by an explicit hook, per test framework:

Run under pytest — auto-cleaned. The bundled plugin (auto-registered on install) calls reset_test_context() after every test; nothing to wire up. This covers unittest.TestCase tests too, since pytest can run them — if your test runner is pytest, you get auto-clean regardless of how the tests are written:

class Services(StaticDeclarativeContainer):
    driver: Driver = sp.TestContextSingleton(make_driver)

# after each test the plugin calls Services.reset_test_context() for you

Run under the stock unittest runner (python -m unittest) — there is no plugin auto-discovery outside pytest, so register the reset yourself with addCleanup:

class Test(unittest.TestCase):
    def setUp(self) -> None:
        self.addCleanup(Services.reset_test_context)

You can also reset manually anywhere: Services.reset_test_context().

Subclassing & whole-container override

Subclass a container to inherit its providers, and redeclare any to replace them:

class Base(StaticDeclarativeContainer):
    db: Database = sp.Singleton(Database)

class Testing(Base):
    db: Database = sp.Singleton(FakeDatabase)   # replaces Base.db for Testing
    extra: Clock = sp.Singleton(Clock)          # adds a provider

Testing.set_overrides(db=other, extra=frozen)   # typed over inherited + own

Override a whole container's providers by name with another container's, undone with reset_override (both reflected in reads):

Base.override(FakeContainer)   # Base.db now resolves FakeContainer.db
Base.reset_override()

Type checking

Reads resolve to the field's type, and set_overrides is fully checked under ty, mypy and pyright (Pylance) — unknown provider names and wrong value types are compile-time errors, verified by a test matrix that runs all three. This requires an annotation on each provider (db: Db = Singleton(Db)) so the checker knows the field type. Direct attribute assignment (Services.db = …) is rejected at runtime — use set_overrides.

Compatibility with dependency-injector

The static, class-level model keeps most of dependency-injector's API, but a few pieces are intentionally different.

Preserved:

  • Introspection — providers, cls_providers, inherited_providers, traverse(), dependencies.
  • Overriding — override(other) / reset_override() / reset_last_overriding() and the overridden tuple, plus the @containers.override(Container) decorator; all reflected in reads.
  • Lazy resolution and per-provider caching semantics.

Deliberately different (because there is no container instance):

  • Reads return values, not providersServices.db is the resolved Database, so Services.db() and provider methods like Services.db.override(...) do not apply. Override with set_overrides / the container-level override; if you need the raw provider, use Services.providers["db"].
  • Instantiation is repurposedServices(db=fake) applies value overrides and returns a restoring handle (usable as a with block); it does not build a container instance.
  • Direct provider assignment (Services.db = provider) is rejected — use set_overrides.
  • init_resources() / shutdown_resources() are deprecated and raise: they are instance-level and meaningless here. A Resource still initializes on first access, but its post-yield teardown is not driven — use Resource for init-only setup, or manage teardown yourself.

Notes & limitations

Because containers are resolved at the class level (no container instance), overrides go through set_overrides (scoped with …: restores on exit; a bare call is permanent), or override(other_container) for a whole-container swap. Both are reflected in reads and cleared by reset_override.

Requirements

  • Python 3.12+ (uses PEP 695 generics)
  • dependency-injector >= 4.49.1

License

MIT. Uses dependency-injector (BSD-3-Clause) as a runtime dependency; none of its source is vendored.

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

static_dependency_injector-0.3.1.tar.gz (8.6 kB view details)

Uploaded Source

Built Distribution

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

static_dependency_injector-0.3.1-py3-none-any.whl (12.3 kB view details)

Uploaded Python 3

File details

Details for the file static_dependency_injector-0.3.1.tar.gz.

File metadata

  • Download URL: static_dependency_injector-0.3.1.tar.gz
  • Upload date:
  • Size: 8.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Artix Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for static_dependency_injector-0.3.1.tar.gz
Algorithm Hash digest
SHA256 14a37d609327bd05696cc64558c915a252463d72e9bce377a6b9d4c23cee17c6
MD5 d1ca27b69f5ccc495f00d14b225ef62f
BLAKE2b-256 496437e2ddb3265bd6aad9273b2a270bbe2e348c51a8cec6263fba2a38a7b364

See more details on using hashes here.

File details

Details for the file static_dependency_injector-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: static_dependency_injector-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 12.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Artix Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for static_dependency_injector-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6e255bddabde2ded18442b8a74d97700c655147e1ff2013a76669e2fbe8b8e63
MD5 64fb951e919f5ebc353665b896bd8e64
BLAKE2b-256 680367c2092cca45f83b528db77f721d0cef8105c9251d4f2309d6ff127c47c6

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