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

Repository files navigation

Wireup

Modern Dependency Injection for Python.

GitHub GitHub Workflow Status (with event) Code Climate maintainability 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


⚡ 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.
  • Simplified use with Django, Flask, and FastAPI.
  • Share service layer between cli and api.

📋 Quickstart

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.

Share service layer betwen app/api and cli

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.

Installation

# Install using poetry:
poetry add wireup

# Install using pip:
pip install wireup

📚 Documentation

For more information check out the documentation

🎮 Demo application

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