Skip to content

Performant, concise, and easy-to-use dependency injection container for Python 3.8+.

License

Notifications You must be signed in to change notification settings

maldoinc/wireup

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Wireup

Modern Dependency Injection for Python.

GitHub GitHub Workflow Status (with event) Coverage PyPI - Python Version PyPI - Version

Wireup is a Performant, concise, and easy-to-use Dependency Injection container for Python 3.8+.

๐Ÿ“š Documentation | ๐ŸŽฎ Demo Application


Dependency Injection (DI) is a design pattern where objects receive their dependencies externally instead of creating them. Wireup manages the creation, injection, and lifecycle management of dependencies. It uses typing to automatically resolve dependencies where required, reducing boilerplate and supports modern Python features such as async and generators.

It can function standalone as a DI container or service locator and also integrates with popular frameworks such as Django, FastAPI and Flask.

โšก Key Features

  • Inject services and configuration.
  • Interfaces and abstract classes.
  • Factory pattern.
  • Singleton and transient dependencies.
  • Framework-agnostic.
  • Apply the container as a decorator.
  • Service Locator.

๐Ÿ“‹ Quickstart

To showcase the basics of Wireup, we will create a container able to inject the following:

  • A WeatherService that queries a fictional weather api, needs an api key and, a KeyValueStore to cache request data and an async http client
  • KeyValueStore itself needs a redis_url denoting the server it will connect to to query/store data.

These services will then be retrieved in a /weather/forecast endpoint that requires WeatherService to provide weather information.

graph LR
    A --> C
    B --> D
    C --> D
    D --> E
    F --> D

    A[โš™๏ธ redis_url]
    B[โš™๏ธ weather_api_key]
    C[๐Ÿ KeyValueStore]
    D[๐Ÿ WeatherService]
    E[๐ŸŒŽ /weather/forecast]
    F[๐Ÿญ HttpClient]
Loading

1. Setup

Install wireup using pip or your favorite package manager.

$ pip install wireup

The first step is to create a container.

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"],
    },
    # Let the container know where service registrations are located.
    service_modules=[services]
)

Parameters are configuration your application needs. Such as an api key, database url, or other settings. Service modules is a list of top-level python modules containing service definitions this container needs to know about.

2. Define services

The container uses configuration metadata from annotations and types to define services and the dependencies between them. This means that the service declaration is self-contained and does not require additional setup for most use cases.

๐Ÿ KeyValueStore

To create KeyValueStore, all we need is the redis_url parameter. The @service decorator tells Wireup this is a service, and we simply need to tell the container via annotated types to fetch the value of the redis_url parameter for dsn.

from wireup import service, Inject
from typing_extensions import Annotated

@service
class KeyValueStore:
    def __init__(self, dsn: Annotated[str, Inject(param="redis_url")]) -> None:
        self.client = redis.from_url(dsn)

๐Ÿ WeatherService

Creating WeatherService is also straightforward. The @service decorator is used to let Wireup know this is a service and we use the same syntax as above for the api_key. Class dependencies do not need additional annotations in this case.

@service
class WeatherService:
    def __init(
        self,
        api_key: Annotated[str, Inject(param="weather_api_key")],
        kv_store: KeyValueStore,
        client: aiohttp.ClientSession,
    ) -> None:
        self.api_key = api_key
        self.kv_store = kv_store

๐Ÿญ aiohttp.ClientSession

The http client making requests cannot be instantiated directly as we need to enter an async context manager. To accomodate 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.

Note: When using generator factories make sure to call container.close (or container.aclose() for async generators) when the application is terminating for the necessary cleanup to take place.

@service
async def make_http_client() -> AsyncIterator[aiohttp.ClientSession]:
    async with aiohttp.ClientSession() as client:
        yield client

Tip

If using annotations is not suitable for your project, you can use factories as shown above to create all dependencies. This lets you keep service definitions devoid of Wireup references.

3. Use

Use the container as a service locator or apply it as a decorator.

The container instance provides an autowire method that when applied to a function will cause the container to pass the dependencies when the function is called.

@app.get("/weather/forecast")
@container.autowire
async def get_forecast_view(weather_service: WeatherService):
    return await weather_service.get_forecast(...)

Alternatively you can use the container's ability to function as a service locator. Simply call .get on the container instance with the type you wish to retrieve.

@app.get("/weather/forecast")
async def get_forecast_view():
    weather_service = container.get(WeatherService)
    return await weather_service.get_forecast(...)

3.5 Integrate

While Wireup is framework-agnostic, usage can be simplified when using it alongside one of the integrations. A key benefit of the integrations, is removing the need to have a global container variable and the need to decorate injection targets in the frameworks.

Each integration also comes with additional goodies specific to that framework.

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.

๐Ÿ“š Documentation

For more information check out the documentation

๐ŸŽฎ Demo application

A demo flask application is available at maldoinc/wireup-demo