Skip to main content

Python Dependency Injection Library

Project description

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 anywhere as a decorator
  • Service Locator
  • Simplified use with Django, Flask, and FastAPI.
  • Share service layer between cli and api.

📋 Quickstart

Example showcasing a Redis wrapper and a weather service that calls an external API and caches results as needed.

1. Set up

from wireup import container, initialize_container

def create_app():
    app = ...

    # ⬇️ Start the container: Register and initialize services.
    initialize_container(
        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]
    )

    return app

2. Register 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:
        ...

3. Inject

Decorate targets where the library should perform injection.

from wireup import container
@app.get("/weather/forecast")
# ⬇️ Decorate functions to perform Dependency Injection.
# Optional in views with Flask or FastAPI integrations.
@container.autowire
def get_weather_forecast_view(weather_service: WeatherService, request):
    return weather_service.get_forecast(request.lat, request.lon)

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.

Flask + Click

Extract from maldoinc/wireup-demo, showing the same service being used in a Flask view as well as in a Click command. Imports omitted for brevity.

App/Api

With the Flask integration, @container.autowire can be omitted.

# blueprints/post.py
@bp.post("/")
def create_post(post_service: PostService) -> Response:
    new_post = post_service.create_post(PostCreateRequest(**flask.request.json))

    return jsonify(new_post.model_dump())

Click CLI

# commands/create_post_command.py
@click.command()
@click.argument("title")
@click.argument("contents")
@container.autowire
def create_post(title: str, contents: str, post_service: PostService) -> None:
    post = post_service.create_post(
        PostCreateRequest(
            title=title, 
            content=contents, 
            created_at=datetime.now(tz=timezone.utc)
        )
    )

    click.echo(f"Created post with id: {post.id}")

@click.group()
def cli() -> None:
    pass


if __name__ == "__main__":
    cli.add_command(create_post)
    initialize_container(
        container, 
        parameters=get_config(), 
        service_modules=[services]
    )
    cli()

Typer CLI

Typer functions a bit differently in that it won't allow unknown arguments in the function signature, so we have to use the wireup container as a service locator.

cli = typer.Typer()

@cli.command()
def create_post(title: str, contents: str) -> None:
    # Using container.get(T) returns an instance of that type.
    post = container.get(PostService).create_post(
        PostCreateRequest(
            title=title, 
            content=contents, 
            created_at=datetime.now(tz=timezone.utc)
        )
    )

    typer.echo(f"Created post with id: {post.id}")


if __name__ == "__main__":
    initialize_container(wireup.container, service_modules=[services], parameters=get_config())
    cli()

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wireup-0.9.1.tar.gz (22.0 kB view hashes)

Uploaded Source

Built Distribution

wireup-0.9.1-py3-none-any.whl (26.3 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page