Getting Started
To showcase the basics of Wireup, we will create a container able to inject the following:
- A
WeatherServicethat queries a fictional weather api. It needs an api key, aKeyValueStoreto cache data and an async http client to make requests. KeyValueStoreitself needs aredis_urldenoting the server it will connect to to query/store data.
These will then be retrieved in a /weather/forecast endpoint that requires WeatherService to provide weather
information.
graph LR
redis_url[⚙️ redis_url] --> KeyValueStore[🐍 KeyValueStore]
weather_api_key[⚙️ weather_api_key] --> WeatherService
KeyValueStore --> WeatherService[🐍 WeatherService]
WeatherService --> Route[🌎 /weather/forecast]
F[🏭 HttpClient] --> WeatherService
Tip
There will be little + icons in code fragments. You can click on those for more detailed information as to what is
happening in that particular line.
1. Define Dependencies¶
The container uses types and annotations to define dependencies and discover relationships between them. This results in self-contained definitions without having to create factories for every dependency.
🐍 KeyValueStore¶
To create the KeyValueStore, we need a value for redis_url. The @injectable decorator registers the class, and the
type hint tells the container to inject the value of the redis_url key into the dsn parameter.
from wireup import injectable, Inject
from typing import Annotated
import redis
@injectable # (1)!
class KeyValueStore:
def __init__(
self, dsn: Annotated[str, Inject(config="redis_url")]
) -> None: # (2)!
self.client = redis.from_url(dsn)
- Decorators are only used to collection metadata. This makes testing simpler, as you can still instantiate this like a regular class in your tests.
- Since type-based injection is not possible here (there can be many string/int configs after all), config injection
must be annotated with the
Inject(config=key)syntax. This tells the container which config key to inject.
🏭 aiohttp.ClientSession¶
The http client making requests cannot be instantiated directly as we need to enter an async context manager. To accommodate such cases, Wireup allows you to use functions to create dependencies. These can be sync/async as well as regular or generator functions if cleanup needs to take place.
Factories can define their dependencies in the function's signature.
When using generator factories make sure to call container.close when the application is terminating for the necessary
cleanup to take place.
from wireup import injectable
from typing import AsyncIterator
import aiohttp
@injectable
async def http_client_factory() -> AsyncIterator[aiohttp.ClientSession]:
async with aiohttp.ClientSession() as client:
yield client
🐍 WeatherService¶
Creating WeatherService is also straightforward. The @injectable decorator creates a unique registration for the
class. Class dependencies do not need additional annotations, even though the http client is created via an async
generator. This is transparently handled by the container.
from wireup import injectable, Inject
from typing import Annotated
import aiohttp
@injectable
class WeatherService:
def __init__(
self,
api_key: Annotated[str, Inject(config="weather_api_key")], # (1)!
kv_store: KeyValueStore, # (2)!
client: aiohttp.ClientSession, # (3)!
) -> None: ...
- Same as above, weather api key needs the config key for the container to inject it.
KeyValueStorecan be injected only by type and does not require annotations.aiohttp.ClientSessioncan be injected only by type and requires no additional configuration.
2. Create the container¶
The next step is to create a container and register the dependencies we just defined.
import wireup
from my_app import services
import os
container = wireup.create_async_container(
# `config` is an optional key-value configuration store.
# You can inject configuration as necessary by referencing config keys.
# This allows you to create self-contained service definitions
# without additional setup code.
config={ # (1)!
"redis_url": os.environ["APP_REDIS_URL"],
"weather_api_key": os.environ["APP_WEATHER_API_KEY"],
},
# Let the container know where registrations are located.
# This is a list of modules containing injectable definitions,
# or functions/classes decorated with `@injectable`.
injectables=[services],
)
-
configis configuration your application needs. Such as an api key, database url, or other settings.You can inject them as necessary by their name (dict key) where required. Wireup won't pull things from the environment or other places for you. You need to expose to it the different settings you'll need.
You don't have to use this if you prefer using things like pydantic-settings, but it will enable you to have self-contained service definitions without writing additional set-up code to create these objects.
Note that the values can be literally anything you need to inject and not just int/strings or other scalars. You can put dataclasses for example in the config to inject structured configuration.
Container variants: Sync and Async
Wireup includes two types of containers: async and sync. The difference is that the async one exposes async def
methods for the common operations and is capable of creating resources from async def factories.
The async container can create both regular and resources from async factories.
If you don't use async in your code you should create a container via wireup.create_sync_container. Some integrations
that Wireup provides also require you to create containers of a given type. E.g: FastAPI integration only supports async
containers.
3. Use¶
All that's left now is to retrieve dependencies from the container.
To fetch dependencies from the container, call .get on the container instance with the type you want to retrieve.
@app.get("/weather/forecast")
async def get_forecast():
weather_service = await container.get(WeatherService)
return await weather_service.get_forecast(...)
You can also apply Wireup containers as decorators. See Apply the container as a decorator docs for more info, but the end result is that you can decorate any function and specify dependencies to inject in it's signature.
from wireup import Injected, inject_from_container
@app.get("/weather/forecast")
@inject_from_container(container)
async def get_forecast(weather_service: Injected[WeatherService]):
return await weather_service.get_forecast(...)
With the FastAPI integration you can declare dependencies in http or websocket routes.
from wireup import Injected
@app.get("/weather/forecast")
async def get_forecast(weather_service: Injected[WeatherService]):
return await weather_service.get_forecast(...)
Learn More: FastAPI Integration.
With the Flask integration you can declare dependencies in views.
from wireup import Injected
@app.get("/weather/forecast")
def get_forecast(weather_service: Injected[WeatherService]):
return weather_service.get_forecast(...)
Learn More: Flask Integration.
With the Django integration you can declare dependencies in views. The integration provides support for async views, regular views as well as class-based views.
from wireup import Injected
async def get_forecast(weather_service: Injected[WeatherService]):
return await weather_service.get_forecast(...)
Learn More: Django Integration.
Integrations¶
While Wireup is framework-agnostic, usage can be simplified when using it alongside one of the integrations. Key benefits of the integrations are:
- Automatic injection in routes without having to do
container.getor use decorators. - Lifecycle management and access to request-scoped dependencies.
- Eliminates the need for a global container variable as containers are bound to the application instance.
Check out the Integrations page.
4. Test¶
Wireup does not patch your classes, which means they can be instantiated and tested independently of the container.
To substitute dependencies on targets such as views in a web application you can override dependencies with new ones on the fly.
with container.override.injectable(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.
Conclusion¶
This concludes the "Getting Started" walkthrough, covering the most common dependency injection use cases.
Info
- Wireup can perform injection on both sync and async targets.
- If you need to create multiple containers, every container you create is separate from the rest and has its own state.