Metadata-Version: 2.4
Name: injex
Version: 1.4.0
Summary: Tiny typed dependency injection for Python apps
Author-email: vshulcz <vshulcz@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/vshulcz/injex
Project-URL: Repository, https://github.com/vshulcz/injex
Project-URL: Documentation, https://vshulcz.github.io/injex/
Project-URL: Issues, https://github.com/vshulcz/injex/issues
Project-URL: Changelog, https://github.com/vshulcz/injex/blob/main/CHANGELOG.md
Keywords: dependency-injection,di,container,ioc,clean-architecture,testing
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Injex

[![Build Status](https://github.com/vshulcz/injex/actions/workflows/ci.yml/badge.svg)](https://github.com/vshulcz/injex/actions/workflows/ci.yml)
[![pypi](https://img.shields.io/pypi/v/injex.svg)](https://pypi.python.org/pypi/injex)
[![Docs](https://img.shields.io/badge/docs-vshulcz.github.io%2Finjex-blue)](https://vshulcz.github.io/injex/)
[![Coverage](https://codecov.io/gh/vshulcz/injex/branch/main/graph/badge.svg)](https://codecov.io/gh/vshulcz/injex)
[![Python Versions](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue)](https://github.com/vshulcz/injex)
[![License](https://img.shields.io/github/license/vshulcz/injex.svg)](https://github.com/vshulcz/injex/blob/main/LICENSE)

Tiny typed dependency injection for Python apps that want explicit wiring without
a framework-sized container.

Injex is for the point where manual constructor calls are still readable in one
place, but start repeating across an API, a worker, a CLI, and tests. It keeps
wiring explicit: normal type hints, zero runtime dependencies, scoped lifetimes,
test overrides, and graph validation before your app starts.

```bash
pip install injex
```

Website: [vshulcz.github.io/injex](https://vshulcz.github.io/injex/)

## How it fits

Define one validated service graph at the composition root, then let every
entrypoint resolve from it instead of re-wiring its own copy.

```mermaid
flowchart LR
  subgraph root["Composition root — one validated graph"]
    direction LR
    S[Settings] --> C[ApiClient]
    C --> R[UserRepository]
    C --> E[EmailSender]
    R --> U[RegisterUser]
    E --> U
  end
  root --> API[FastAPI]
  root --> CLI[Typer CLI]
  root --> WK[Worker]
  root --> TS[Tests]
```

## The problem it solves

Without a composition root, the same object graph often leaks into every
entrypoint:

```python
repo = UserRepository(settings.database_url)
mailer = EmailSender(settings.smtp_url)
use_case = RegisterUser(repo, mailer)
```

That is fine once. It becomes harder to maintain when the API, background jobs,
CLI commands, and tests all need the same graph with small differences.

With Injex, application code keeps normal constructors and startup code owns the
wiring:

```python
container = Container()
container.add_instance(Settings, settings)
container.add_singleton(UserRepository)
container.add_singleton(EmailSender)
container.add_transient(RegisterUser)

container.assert_valid()

use_case = container.resolve(RegisterUser)
```

Tests can replace one dependency without changing production registrations:

```python
with container.override(EmailSender, instance=fake_mailer):
    use_case = container.resolve(RegisterUser)
```

## Use Injex when

- you have a service layer reused by an API, CLI, worker, and tests;
- constructors already describe dependencies with type hints;
- test doubles should replace external services without changing production wiring;
- startup should catch missing registrations before the first request or job.

## Skip Injex when

- a few manual constructor calls are still clear enough;
- your framework dependency system already covers every entrypoint;
- you need a large provider/configuration DSL.

## Why Injex?

- **Zero dependencies**: pure Python, easy to vendor, audit, and run anywhere.
- **Typed constructor injection**: dependencies are resolved from annotations.
- **Framework-agnostic**: use the same wiring in web apps, workers, CLIs, and
  tests.
- **Production lifetimes**: singleton, transient, and scoped services.
- **Factories and instances**: use custom creation logic or prebuilt objects.
- **Named registrations**: register multiple implementations of the same type.
- **Optional dependencies**: `Optional[T]` works without special configuration.
- **Test overrides**: swap real services for fakes in a small, explicit scope.
- **Container validation**: catch missing annotations, missing registrations, and
  dependency cycles before your app starts.
- **Fast hot-path resolution**: cached dependency plans keep repeated resolves
  close to manual wiring for small service graphs.

## Performance snapshot

Injex compiles and caches simple dependency plans, then uses a fast path
for common constructor-injection graphs. In a small synthetic graph with a
singleton `Settings`, singleton `ApiClient`, and transient repository/service
objects, Injex resolves faster than several popular Python DI containers on this
machine.

| Library | Median resolve time |
| --- | ---: |
| manual wiring | `0.271 µs/op` |
| Injex | `0.401 µs/op` |
| Wireup, same scope | `0.910 µs/op` |
| Wireup, scope per operation | `1.605 µs/op` |
| dependency-injector | `1.776 µs/op` |
| lagom | `10.392 µs/op` |
| punq | `59.662 µs/op` |

Benchmarks are synthetic and not a universal ranking. They are included to show
the approximate overhead of Injex in its target shape: small explicit service
graphs reused by APIs, workers, CLIs, and tests.

Reproduce locally:

```bash
uv run --with punq --with lagom --with dependency-injector --with wireup \
  python benchmarks/resolve_graph.py
```

## Where it fits

Injex is useful when manual wiring starts to spread across your entrypoints, but
`providers`, global state, or a framework-specific container would be too much.

Common patterns:

- **Service layer**: wire repositories, gateways, clients, and use cases once at
  startup.
- **CLIs**: share configuration, API clients, and commands without module-level
  singletons.
- **Workers**: create one scope per job or message while reusing long-lived
  clients.
- **Tests**: override slow or external dependencies inside one `with` block.
- **Clean architecture**: keep application code depending on interfaces instead
  of framework-specific dependency hooks.

## Quick start

```python
from injex import Container


class UserRepository:
    def save(self, email: str) -> int:
        return 42


class EmailSender:
    def send_welcome(self, email: str) -> None:
        print(f"Welcome, {email}")


class RegisterUser:
    def __init__(self, repo: UserRepository, email_sender: EmailSender):
        self.repo = repo
        self.email_sender = email_sender

    def execute(self, email: str) -> int:
        user_id = self.repo.save(email)
        self.email_sender.send_welcome(email)
        return user_id


container = Container()
container.add_singleton(UserRepository)
container.add_singleton(EmailSender)
container.add_transient(RegisterUser)

container.assert_valid()

use_case = container.resolve(RegisterUser)
user_id = use_case.execute("ada@example.com")
```

## Validate wiring before startup

`validate()` checks the registered dependency graph without constructing your
services. That makes it safe for startup checks and CI smoke tests.

```python
errors = container.validate()

if errors:
    for error in errors:
        print(error)
    raise SystemExit(1)
```

Use `assert_valid()` when you prefer a single exception with all validation
errors.

## Testing with overrides

Use `override()` to replace a dependency only inside a `with` block.

```python
class FakeEmailSender:
    def __init__(self):
        self.sent_to = []

    def send_welcome(self, email: str) -> None:
        self.sent_to.append(email)


fake_sender = FakeEmailSender()

with container.override(EmailSender, instance=fake_sender):
    use_case = container.resolve(RegisterUser)
    use_case.execute("test@example.com")

assert fake_sender.sent_to == ["test@example.com"]
```

## Scopes for request-style lifetimes

Scoped services are reused inside one scope and recreated for another scope.

```python
from injex import Container


class RequestContext:
    pass


container = Container()
container.add_scoped(RequestContext)

scope_a = container.create_scope()
scope_b = container.create_scope()

assert scope_a.resolve(RequestContext) is scope_a.resolve(RequestContext)
assert scope_a.resolve(RequestContext) is not scope_b.resolve(RequestContext)
```

## Feature comparison

| Feature | Injex | dependency-injector | punq | lagom |
| --- | ---: | ---: | ---: | ---: |
| Zero runtime dependencies | ✅ | ❌ | ✅ | ✅ |
| Type-hint constructor injection | ✅ | ✅ | ✅ | ✅ |
| Singleton / transient lifetimes | ✅ | ✅ | ✅ | ✅ |
| Scoped lifetime | ✅ | ✅ | ❌ | ✅ |
| Named registrations | ✅ | ✅ | ❌ | ✅ |
| Property injection | ✅ | ❌ | ❌ | ❌ |
| Temporary test overrides | ✅ | ✅ | ❌ | ✅ |
| Graph validation without object creation | ✅ | ❌ | ❌ | ❌ |
| Small API surface | ✅ | ❌ | ✅ | ✅ |

This table is not a benchmark. It shows the niche: Injex aims to be small and
explicit while still covering common application wiring needs.

## Documentation and examples

- [Docs site](https://vshulcz.github.io/injex/)
- [Docs index](./docs/index.md)
- [Tutorial](./docs/tutorial.md)
- [Validation guide](./docs/validation.md)
- [Why Injex](./docs/why-injex.md)
- [Comparison guide](./docs/comparison.md)
- [Compared to FastAPI Depends](./docs/fastapi-depends.md)
- [Compared to larger DI frameworks](./docs/di-frameworks.md)
- [Performance notes](./docs/performance.md)
- [Recipes](./docs/recipes.md)
- [Migrating from a factories module](./docs/migrating-from-factories.md)
- [Usage scenarios](./docs/usage-scenarios.md)
- [API reference](./docs/api.md)
- [Article: When Python manual wiring turns into copy-paste architecture](https://vshulcz.hashnode.dev/when-python-manual-wiring-turns-into-copy-paste-architecture)
- [Article: Where should dependency wiring live in a Python app?](https://vshulcz.hashnode.dev/where-should-dependency-wiring-live-in-a-python-app)
- [DEV: Fast dependency injection in Python without a provider framework](https://dev.to/vshulcz/fast-dependency-injection-in-python-without-a-provider-framework-3dko)
- [X launch post](https://x.com/vshulcz_dev/status/2064249921570533567?s=20)
- [X project thread](https://x.com/vshulcz_dev/status/2059720295536009683?s=20)
- [Clean architecture example](./examples/clean_architecture.py)
- [CLI application example](./examples/cli_app.py)
- [FastAPI application service example](./examples/fastapi_app.py)
- [FastAPI lifespan composition root example](./examples/fastapi_lifespan.py)
- [Testing overrides example](./examples/testing.py)
- [Scoped lifetime example](./examples/scoped.py)
- [Factories example](./examples/factory.py)
- [Named registrations example](./examples/named.py)
- [Benchmark source and notes](./benchmarks/README.md)

## API at a glance

| Method | Use when |
| --- | --- |
| `add_singleton(T, Impl)` | One instance should be reused for the app lifetime. |
| `add_transient(T, Impl)` | A new instance should be created on every resolve. |
| `add_scoped(T, Impl)` | One instance should be reused inside one scope. |
| `add_*_factory(T, factory)` | Construction needs custom code. |
| `add_instance(T, instance)` | You already have the object to use. |
| `resolve(T)` | Resolve one service from the root container. |
| `resolve_all(T)` | Resolve all unnamed implementations for a type. |
| `create_scope()` | Start a request, job, or message lifetime. |
| `override(T, ...)` | Temporarily replace a dependency in tests. |
| `validate()` / `assert_valid()` | Check wiring before startup. |

## Common use cases

- Service-layer wiring in web APIs without coupling code to a web framework.
- Clean architecture use cases with repositories, gateways, and presenters.
- CLI tools where commands share configuration, clients, and services.
- Background workers and consumers with per-job or per-message scopes.
- Unit tests that need explicit dependency replacement.

## Contributors

Thanks to the people improving Injex through issues, reviews, and pull requests:

- [Muhammad Saqib Atif](https://github.com/msaqibatifj) — FastAPI example.
- [mahek](https://github.com/mahek56) — `resolve_all()` documentation recipe.
- [oppnc](https://github.com/oppnc) — nested override regression tests.
- [YuuGR1337](https://github.com/YuuGR1337) — README article link.

## Contributing

Contributions are welcome when they keep the API small, tested, and practical.
Useful changes usually improve documentation, typing, examples, or narrow edge
cases without adding runtime dependencies.

See [CONTRIBUTING.md](./CONTRIBUTING.md) for the local setup and contribution
guidelines.
