Modern Dependency Injection for Python.
Wireup is a performant, concise, and easy-to-use dependency injection container for Python 3.8+.
- Inject services and configuration.
- Interfaces and abstract classes.
- Factory pattern.
- Singleton and transient dependencies.
- Framework-agnostic.
- Apply the container as a decorator.
- Service Locator.
- Simplified use with Django, Flask, and FastAPI.
- Share service layer between cli and api.
1. Set up
import wireup
container = wireup.create_container(
# Parameters serve as application/service configuration.
parameters={
"redis_url": os.environ["APP_REDIS_URL"],
"weather_api_key": os.environ["APP_WEATHER_API_KEY"]
},
# Top-level modules containing service registrations.
service_modules=[services]
)
2. Declare services
Use a declarative syntax to describe services, and let the container handle the rest.
from wireup import service, Inject
@service # ⬅️ Decorator tells the container this is a service.
class KeyValueStore:
# Inject the value of the parameter during creation. ⬇️
def __init__(self, dsn: Annotated[str, Inject(param="redis_url")]):
self.client = redis.from_url(dsn)
def get(self, key: str) -> Any: ...
def set(self, key: str, value: Any): ...
@service
@dataclass # Can be used alongside dataclasses to simplify init boilerplate.
class WeatherService:
# Inject the value of the parameter to this field. ⬇️
api_key: Annotated[str, Inject(param="weather_api_key")]
kv_store: KeyValueStore # ⬅️ This will be injected automatically.
def get_forecast(self, lat: float, lon: float) -> WeatherForecast:
...
Use factories (sync and async) if service requires special initialization or cleanup.
@service
async def make_db(dsn: Annotated[str, Inject(param="db_dsn")]) -> AsyncIterator[Connection]:
async with Connection(dsn) as conn:
yield conn
Note: If you use generator factories, call container.{close,aclose}
on termination for the necessary cleanup to take place.
3. Use
Use the container as a service locator or apply it as a decorator to have it perform injection.
weather_service = container.get(WeatherService)
@app.get("/weather/forecast")
# ⬇️ Decorate functions to perform Dependency Injection.
# No longer required when using the provided integrations.
@container.autowire
def get_weather_forecast_view(weather_service: WeatherService, request):
return weather_service.get_forecast(request.lat, request.lon)
4. Test
Wireup does not patch your services which means they can be instantiated and tested independently of the container.
To substitute dependencies on autowired targets such as views in a web application you can override dependencies with new ones on the fly.
with container.override.service(WeatherService, new=test_weather_service):
response = client.get("/weather/forecast")
Requests to inject WeatherService
during the lifetime of the context manager
will result in test_weather_service
being injected instead.
Many projects have a web application as well as a cli in the same project which provides useful commands.
Wireup makes it extremely easy to share the service layer between them without code duplication. For examples refer to maldoinc/wireup-demo.
# Install using poetry:
poetry add wireup
# Install using pip:
pip install wireup
For more information check out the documentation
A demo flask application is available at maldoinc/wireup-demo