Django integration¶
The priority integration. If you have to pick one framework to try this library with, pick Django.
Installation¶
Two ways to bootstrap¶
Project-wide auto-discovery (recommended for typical projects, especially
those with many domain apps) — one INSTALLED_APPS entry plus a settings
dict. Spring Boot–style. See Project-wide auto-discovery
below.
Per-app AutowiredAppConfig (for apps that own their DI scanning, or for
reusable apps published as packages). See Per-app config below.
You can use either independently — but not both at once in the same project (that would double-initialize the container).
Project-wide auto-discovery¶
Add the integration to INSTALLED_APPS and define a DJANGO_AUTOWIRED
settings dict:
# settings.py
INSTALLED_APPS = [
...,
"myproject.users",
"myproject.billing",
"myproject.notifications",
"django_autowired.integrations.django", # adds AutowiredDjangoConfig
]
DJANGO_AUTOWIRED = {
"AUTODISCOVER_PREFIX": "myproject.", # scan every shop.* app's adapters/
"AUTODISCOVER_SUBPACKAGE": "adapters", # optional: only scan <app>/adapters/
"BACKEND": "injector", # default
"EXTRA_MODULES": [ # optional: dotted "module:attr" specs
"myproject.di:database_module",
],
"EXCLUDE": {"legacy"}, # optional: extra exclude_patterns
"STRICT": True, # optional: raise on broken imports
}
That's it. Adding a new domain app is just:
- Append it to
INSTALLED_APPS. - Decorate its services with
@injectable(or@providesfor conditional bindings).
No per-app DI config required.
Settings keys¶
| Key | Type | Default | Description |
|---|---|---|---|
PACKAGES |
list[str] |
— | Explicit list of dotted package paths. Mutually exclusive with AUTODISCOVER_PREFIX. |
AUTODISCOVER_PREFIX |
str |
— | Filter INSTALLED_APPS to entries whose package name starts with this prefix. Empty string scans every non-Django app. |
AUTODISCOVER_SUBPACKAGE |
str \| None |
None |
Sub-path appended to each discovered app (e.g. "adapters"). Apps lacking the sub-path are silently skipped — useful for incremental adoption. |
BACKEND |
str |
"injector" |
DI backend name. |
EXTRA_MODULES |
list[str] |
[] |
Backend-specific modules as "module.path:attr" strings, resolved by import. |
EXCLUDE |
set[str] |
set() |
Additional module-name segments to skip during scanning. |
STRICT |
bool |
False |
If True, the scanner raises on broken sub-modules. Recommended for CI. |
Spring Boot mental model¶
| Spring Boot | django-autowired |
|---|---|
@SpringBootApplication |
INSTALLED_APPS += ["django_autowired.integrations.django"] |
@ComponentScan(basePackages = "com.shop") |
DJANGO_AUTOWIRED["AUTODISCOVER_PREFIX"] = "shop." |
@Service / @Repository / @Component |
@injectable |
@Configuration + @Bean |
A module containing @provides factories |
application.properties |
DJANGO_AUTOWIRED + Django settings |
A complete worked example lives in
examples/django_autodiscover_demo/.
Per-app config¶
1. Subclass AutowiredAppConfig:
# myapp/apps.py
from django_autowired.integrations.django import AutowiredAppConfig
class MyAppConfig(AutowiredAppConfig):
name = "myapp"
autowired_packages = ["myapp.services", "myapp.adapters"]
autowired_backend = "injector" # default
2. Wire it up in myapp/__init__.py:
3. Add the app to INSTALLED_APPS:
That's it. At Django startup, ready() triggers the scan and builds the
container.
Structuring packages for scanning¶
myapp/
├── __init__.py
├── apps.py
├── services/
│ ├── __init__.py
│ ├── order_service.py # @injectable()
│ └── email/
│ └── notifier.py # @injectable()
├── adapters/
│ ├── __init__.py
│ └── out_/
│ ├── __init__.py
│ ├── sql_user_repo.py # @injectable(bind_to=IUserRepository)
│ └── stripe_gateway.py # @injectable(bind_to=IPaymentGateway)
├── ports/
│ ├── __init__.py
│ ├── user_repository.py # IUserRepository(ABC)
│ └── payment_gateway.py # IPaymentGateway(ABC)
├── migrations/ # auto-skipped
└── tests/ # auto-skipped
List each branch explicitly:
Django-specific bindings (extra_modules)¶
Wrap Django primitives in an injector.Module:
import injector
from django.conf import settings
from django.core.cache import cache
from django.db import connection
class DjangoModule(injector.Module):
def configure(self, binder):
binder.bind(type(cache), to=cache)
binder.bind(type(connection), to=connection)
# Config values
binder.bind_scalar("secret_key", to=settings.SECRET_KEY)
class MyAppConfig(AutowiredAppConfig):
name = "myapp"
autowired_packages = ["myapp.services", "myapp.adapters"]
autowired_extra_modules = [DjangoModule()]
Using injected services in views / tasks¶
# myapp/views.py
from django.http import JsonResponse
from django_autowired import container
from myapp.services.order_service import OrderService
def place_order(request, sku: str):
svc = container.get(OrderService)
order = svc.place(sku)
return JsonResponse({"id": order.id})
Testing with Django's test runner¶
# myapp/tests/test_order_service.py
import pytest
from django_autowired import container
from myapp.services.order_service import OrderService
@pytest.mark.autowired_packages(["myapp.services", "myapp.adapters"])
@pytest.mark.autowired_backend("injector")
def test_order_service(autowired_container):
svc = container.get(OrderService)
assert svc is not None
Compatibility¶
| Django version | Status |
|---|---|
| 5.1 | ✅ |
| 5.0 | ✅ |
| 4.2 LTS | ✅ |
FAQ¶
Why not use Django's built-in signals for this?
Signals are an event bus, not a dependency container. They don't solve constructor injection, interface binding, or scope management. This library composes cleanly with signals if you use both.
Does AutowiredAppConfig run on every request?
No. ready() runs once at process start, just like any other AppConfig.ready().
Can I use this with Django's built-in injector patterns (if I already have them)?
Yes. Put your existing Module subclasses into autowired_extra_modules and
they'll be merged with the scanned registrations.