GithubHelp home page GithubHelp logo

maldoinc / wireup Goto Github PK

View Code? Open in Web Editor NEW
83.0 3.0 1.0 5.15 MB

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

Home Page: https://maldoinc.github.io/wireup/

License: MIT License

Python 99.34% Makefile 0.66%
dependency-injection python django flask injector dependency-injection-container dependency-injector

wireup's Introduction

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

wireup's People

Contributors

dependabot[bot] avatar maldoinc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

jod35

wireup's Issues

Error injecting impl from interface when one of the qualifiers is None

When there are multiple implementations for an interface and one of them has None as a qualifier, then that one gets injected when asking for the interface without any additional info.

Automatically injecting impl from interface is supported only when there is one impl. In this case user should use a qualifier. Although not technically a bug because qualifier None does exist.

def test_two_qualifiers_are_injected(self):
    @self.container.autowire
    # This should error
    def inner(sub1: FooBase, sub2: FooBase = wire(qualifier="sub2")):
        self.assertEqual(sub1.foo, "bar")
        self.assertEqual(sub2.foo, "baz")

    self.container.abstract(FooBase)
    self.container.register(FooBar)
    self.container.register(FooBaz, qualifier="sub2")
    inner()

Inconsistent usage of Wire() in fastapi and other frameworks

When using fastapi, annotating services with Wire() while required -- is the equivalent of having no annotations when used in other frameworks.

This needs to change so that the behavior is consistent and that since the type is annotated, might as well get the full benefits of the annotation such as throwing when requesting things that don't exist.

Add locking for multithread applications

Something like

if (proxy := getattr(self, "_ContainerProxy__proxy_object")) is None:
    with getattr(self, "_ContainerProxy__lock"):
        if (proxy := getattr(self, "_ContainerProxy__proxy_object")) is None:
            proxy = getattr(self, "_ContainerProxy__supplier")()
            super().__setattr__("_ContainerProxy__proxy_object", proxy)

Looks like __create_instance will also need updated with locking.

Unit Testing Flask Route Handlers

Hiya! Greatly enjoying this lib; we are exploring DI in one of our services and I've got a PR to add wireup and just use the singleton; the only issue the team has run into is we're not able to inject a mock into the flask route handler. Normally, like with services, we just directly use the target object and instantiate it in the unit test with mocks, however this won't work for flask endpoints. Consider the following code

@app.route("/fake/route/<route_param_one>")
@container.autowire
def import_setup(
    route_param_one: str, greeter: GreeterService
) -> Response:
    name = request.args.get("name")
    greeter.greet(name, route_param_one)

    return make_response("OK")

And this unit test with flask's test app mechanism

class TestImportSetup(TestCase):
    def setUp(self) -> None:
        self.app = app.test_client()

    def test_missing_vendor(self) -> None:
        result = self.app.get("/fake/route/fakedata?name=Cody")

        # How to provide a fake GreeterService if app entry point has already wired it up?
        self.assertEqual(200, result.status_code)
        self.assertEqual("Ok", result.text)

I've tried setting up multiple containers to have an empty test one and a normal prod one but it has proven... quite difficult to use.

Could we perhaps have a context manager style solution?

class TestImportSetup(TestCase):
    def setUp(self) -> None:
        self.app = app.test_client()
        self.mock_greeter = Mock()

    def test_missing_vendor(self) -> None:
        with container.override_inject(GreeterService, self.mock_greeter):
            result = self.app.get("/fake/route/fakedata?name=Cody")

            self.mock_greeter.assert_called_once_with("Cody", "fakedata")
            self.assertEqual(200, result.status_code)
            self.assertEqual("Ok", result.text)

FastAPI integration raises deprecation warning

A deprecation warning is being raised when calling the FastApi integration.

PATH/lib/python3.12/site-packages/wireup/integration/fastapi_integration.py:34: UserWarning: Using warmup_container is deprecated. Use 'initialize_container' instead
  warmup_container(dependency_container, service_modules or [])

Pass known singeltons/parameters when autowiring

The @container.autowire decorator will simply return a new function that will bind container objects to the function on call. Since this will happen on every call, even though cached it still has a small performance penalty.

On registration, if the container has been warmed up we should bind any known singleton services and parameters as the container is considered final.

Add an easy way to override dependencies

Sometimes when the target method is already autowired it can be a bit difficult to substitute a service for a different implementation.

Add a way for users to override dependencies, preferrably through a ctx manager if they wish to do so.

See #7

Add support for async factories

Will have to detect if the factory is async or not and then use an async proxy

if (proxy := getattr(self, "_AsyncContainerProxy__proxy_object")) is None:
    async with getattr(self, "_AsyncContainerProxy__lock"):
        if (proxy := getattr(self, "_AsyncContainerProxy__proxy_object")) is None:
            proxy = await getattr(self, "_AsyncContainerProxy__supplier")()
            super().__setattr__("_AsyncContainerProxy__proxy_object", proxy)

Allow providing arguments to autowired methods

If an arg has already been provided, the container can skip it. This should enable calling a function with an argument provided by the caller that the container would have otherwise passed.

A use case might be calling flask functions with new dependencies during tests when testing the endpoints as functions rather than e2e.

Should probably emit a log.info for this.

# Flask example

@app.get("/")
@container.autowire
def home(greeter: GreeterService):
    ...


# After: This should be possible
home(greeter=DummyGreeter())

Usage in fastapi is broken

Both fastapi and wireup currently supply a value for the parameter leading to a runtime error when the function is called

Add support for protocols

Allow injection of types based on protocols. Services can declare themselves what protocol they support and the container can then perform autowiring based on that.

class SupportsFoo(Protocol):
    def foo(self) -> str:
        pass


@container.register(supports=SupportsFoo)
class FooBar:
    def foo(self):
        return "bar"

Requesting SupportsFoo will have FooBar injected.

Add support for generator factories

I want to do something like the following:

@service
def create_transaction() -> Iterator[Transaction]:
    transaction = Transaction()
    try:
        yield transaction
    finally:
        transaction.commit()

and also an async version:

@service
async def create_transaction() -> AsyncIterator[Transaction]:
    transaction = Transaction()
    try:
        yield transaction
    finally:
        await transaction.commit()

Unable to register a service using a qualifier in a factory function

The following registration fails due to service FooBar being registered twice since qualifier is ignored for factories. Factories should also be able to specify qualifiers.

@container.register(qualifier="1")
def foo_factory() -> FooBar:
    return FooBar()

@container.register(qualifier="2")
def foo_factory2() -> FooBar:
    return FooBar()

Add abstract injection without importing concrete impl

Currently from my experience, when we are using Annotated[AbstractClass, Wire(qualifier=Any)] - we still need to import redundant Concrete Impl to scope, where @autowire is performed. If such import is not performed, user will experience
wireup.errors.UnknownQualifiedServiceRequestedError

It would be nice for this library to have such feature, or at least, have an docs for this cause, to not import redundant concrete classes, but have a workaround.

Introduce functional deps

They can ask for dependencies as usual and return a function that will perform the task.

@container.functional
def greeter_factory(translator_service: TranslatorService, other_service ...) -> Callable...
    def greeter(name: str, locale: str = 'en_GB') -> str:
        return ...

    return greeter

@container.autowire
def target(greeter: Annotated[Callable..., Wire(fn=greeter_factory))
    ...

Add support for scoped lifetimes

When using in a web app it can be beneficial to have request-scoped objects to make sure they are initialized only once per request.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.