GithubHelp home page GithubHelp logo

anydi's Introduction

AnyDI

Modern, lightweight Dependency Injection library using type annotations.

CI Coverage Documentation


Documentation

http://anydi.readthedocs.io/


AnyDI is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.8+, based on type annotations (PEP 484).

The key features are:

  • Type-safe: Resolves dependencies using type annotations.
  • Async Support: Compatible with both synchronous and asynchronous providers and injections.
  • Scoping: Supports singleton, transient, and request scopes.
  • Easy to Use: Designed for simplicity and minimal boilerplate.
  • Named Dependencies: Supports named dependencies using Annotated type.
  • Resource Management: Manages resources using context managers.
  • Modular: Facilitates a modular design with support for multiple modules.
  • Scanning: Automatically scans for injectable functions and classes.
  • Integrations: Provides easy integration with popular frameworks and libraries.
  • Testing: Simplifies testing by allowing provider overrides.

Installation

pip install anydi

Quick Example

app.py

from anydi import auto, Container

container = Container()


@container.provider(scope="singleton")
def message() -> str:
    return "Hello, world!"


@container.inject
def say_hello(message: str = auto) -> None:
    print(message)


if __name__ == "__main__":
    say_hello()

FastAPI Example

app.py

from fastapi import FastAPI

import anydi.ext.fastapi
from anydi import Container
from anydi.ext.fastapi import Inject

container = Container()


@container.provider(scope="singleton")
def message() -> str:
    return "Hello, World!"


app = FastAPI()


@app.get("/hello")
def say_hello(message: str = Inject()) -> dict[str, str]:
    return {"message": message}


# Install the container into the FastAPI app
anydi.ext.fastapi.install(app, container)

Django Ninja Example

container.py

from anydi import Container


def get_container() -> Container:
    container = Container()

    @container.provider(scope="singleton")
    def message() -> str:
        return "Hello, World!"

    return container

settings.py

INSTALLED_APPS = [
    ...
    "anydi.ext.django",
]

ANYDI = {
    "CONTAINER_FACTORY": "myapp.container.get_container",
    "PATCH_NINJA": True,
}

urls.py

from django.http import HttpRequest
from django.urls import path
from ninja import NinjaAPI

from anydi import auto

api = NinjaAPI()


@api.get("/hello")
def say_hello(request: HttpRequest, message: str = auto) -> dict[str, str]:
    return {"message": message}


urlpatterns = [
    path("api/", api.urls),
]

anydi's People

Contributors

antonrh avatar

Stargazers

Myron Machado avatar Umar Farouk Umar avatar Anastasiia Kleimonova avatar Boris Uvarov avatar Iván Yepes avatar Maggie Walker avatar Per Øyvind Øygard avatar Jessica Chung avatar Fuad Asadullayev avatar Oleh Zhovnuvatyi avatar Aleksandr Mehehdenko avatar Peter DeVita avatar  avatar  avatar  avatar  avatar Vladyslav Fedoriuk avatar  avatar Telvis Calhoun avatar Chris Goddard avatar  avatar Andrei Sukhomozgii avatar  avatar Rain Bõmberg avatar Denis J. Cirulis avatar Deniss Cirulis avatar Daniel Travin avatar Slava Kalichonak avatar Dima Urukov avatar Aleksandr Kozlov avatar Genrih avatar Kirill avatar Pavel Lavreshin avatar Pavel avatar

Watchers

 avatar  avatar

anydi's Issues

[version 26.2] AnyDi preinitializes providers in request context even if they are not actually needed

In the latest 26.2 version (probably already in 26.0), even though the resource handling is improved and request-scoped resources are properly closed now, on the other hand the framework pre-initializes some providers (resources) that are not needed to handle the specific request. If these resources are heavier to create, then the request serving time is affected.

I think this is done here: anydi._context.ResourceScopedContext.astart

    async def astart(self) -> None:
        """Start the scoped context asynchronously."""
        for interface in self.container._providers_cache.get(self.scope, []):  # noqa
            await self.container.aresolve(interface)

Where container._providers_cache contains seemingly unrelated providers to the current request's required dependencies.

Support for different request-scoped providers and instances

[Feature request]
Support for different request-scoped providers and instances

Sometimes there is a need to register a different provider and/or instance for each request context. An example for this would be a request-level parameter, config or state that needs to be shared with other request-scoped services easily.

Currently this is not possible, because
a. only one provider can be registered globally for the request-scope and/or
b. registration of instances is not supported.

One possible workaround at the moment is to manually register data on the current protected request context of the container.
Assumedly, the target solution should be something very similar, but better exposed by the framework, for example:
a. introducing the concept of registering parameters, configs or instances for the different scopes; which automatically means that for request-scope a new, different instance can be bound every time on a request context
b. introducing support for registering a different provider for each request context instead of just globally for the request-scope.

Please see two runnable example snippets for:

  1. The problem statement:
import time
from threading import Thread
from typing import Annotated

import anydi
import requests
import uvicorn
from anydi import Container
from anydi.ext.fastapi import Inject
from anydi.ext.starlette.middleware import RequestScopedMiddleware
from fastapi import FastAPI, Path
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response
from starlette.types import ASGIApp


class SomeOtherService:
    pass


class ShoppingCartService:
    def __init__(self,
                 shopping_cart_id: Annotated[str, "shopping_cart_id"],
                 some_other_service: SomeOtherService):
        self.shopping_cart_id = shopping_cart_id
        self.some_other_service = some_other_service
        self.items = []

    def add_item(self, item: str):
        self.items.append(item)

    def order_items(self):
        return f"Order for shopping cart {self.shopping_cart_id} has been placed. Items: {self.items}"


class ShoppingCartContextMiddleware(BaseHTTPMiddleware):

    def __init__(self, app: ASGIApp, container: Container) -> None:
        super().__init__(app)
        self.container = container

    async def dispatch(
            self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        path = request.url.path
        parts = path.split('/')
        shopping_cart_id = parts[1]  # a more robust parsing is needed in real-life

        # [FEATURE REQUEST]
        # THIS IS NOT POSSIBLE AT THE MOMENT
        # (to register a new provider, or instance, only in the scope of the current request-context)
        self.container.register(Annotated[str, "shopping_cart_id"], lambda: shopping_cart_id, scope="request")

        return await call_next(request)


container = Container()

app = FastAPI(middleware=[
    Middleware(RequestScopedMiddleware, container=container),
    Middleware(ShoppingCartContextMiddleware, container=container),
])


@app.get("/{shopping_cart_id}/order-test")
async def order_test(shopping_cart_id: str = Path(),
                     # instead of manually passing `shopping_cart_id` everywhere, injection would be preferred
                     cart_service: ShoppingCartService = Inject()) -> str:
    cart_service.add_item("test item 1")
    cart_service.add_item("test item 2")
    result = cart_service.order_items()
    return result


anydi.ext.fastapi.install(app, container)


def run_server():
    uvicorn.run(app, host="0.0.0.0", port=8000)


if __name__ == "__main__":
    thread = Thread(target=run_server)
    thread.daemon = True
    thread.start()

    # Give the server a moment to start
    print("Waiting for server to start...")
    time.sleep(3)
    print("Sending test request...")

    response = requests.get("http://127.0.0.1:8000/my-shopping-cart-1/order-test")
    print(response.text)

    response = requests.get("http://127.0.0.1:8000/my-shopping-cart-1/order-test")
    print(response.text)
    # second invocation raises: LookupError: The provider interface `Annotated[str, "shopping_cart_id"]]` already registered.
    # because the provider registration happens globally and not just inside the current scope (request context)
    # see ShoppingCartContextMiddleware for the root cause
  1. The workaround:
import time
from threading import Thread
from typing import Annotated

import anydi
import requests
import uvicorn
from anydi import Container
from anydi.ext.fastapi import Inject
from anydi.ext.starlette.middleware import RequestScopedMiddleware
from fastapi import FastAPI, Path
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response
from starlette.types import ASGIApp


class SomeOtherService:
    pass


class ShoppingCartService:
    def __init__(self,
                 shopping_cart_id: Annotated[str, "shopping_cart_id"],
                 some_other_service: SomeOtherService):
        self.shopping_cart_id = shopping_cart_id
        self.some_other_service = some_other_service
        self.items = []

    def add_item(self, item: str):
        self.items.append(item)

    def order_items(self):
        return f"Order for shopping cart {self.shopping_cart_id} has been placed. Items: {self.items}"


class ShoppingCartContextMiddleware(BaseHTTPMiddleware):

    def __init__(self, app: ASGIApp, container: Container) -> None:
        super().__init__(app)
        self.container = container

    async def dispatch(
            self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        path = request.url.path
        parts = path.split('/')
        shopping_cart_id = parts[1]  # a more robust parsing is needed in real-life

        # ------ WORKAROUND PART 1 ------
        # noinspection PyProtectedMember
        rc = self.container._get_request_context()
        rc.shopping_cart_id = shopping_cart_id

        return await call_next(request)


container = Container()

app = FastAPI(middleware=[
    Middleware(RequestScopedMiddleware, container=container),
    Middleware(ShoppingCartContextMiddleware, container=container),
])


# ------ WORKAROUND PART 2 ------
@container.provider(scope="request")
def provide_shopping_cart_id() -> Annotated[str, "shopping_cart_id"]:
    """
    Provides the `shopping_cart_id` in the current request context, if available, otherwise raises error.
    See `ShoppingCartContextMiddleware` as a prerequisite.
    """
    shopping_cart_id = None
    # noinspection PyProtectedMember
    rc = container._get_request_context()
    if hasattr(rc, "shopping_cart_id"):
        shopping_cart_id = rc.shopping_cart_id

    if not shopping_cart_id:
        raise LookupError("The shopping_cart_id parameter has not been set up correctly on the request context!")
    return shopping_cart_id


@app.get("/{shopping_cart_id}/order-test")
async def order_test(shopping_cart_id: str = Path(),
                     # instead of manually passing `shopping_cart_id` everywhere, injection would be preferred
                     cart_service: ShoppingCartService = Inject()) -> str:
    cart_service.add_item("test item 1")
    cart_service.add_item("test item 2")
    result = cart_service.order_items()
    return result


anydi.ext.fastapi.install(app, container)


def run_server():
    uvicorn.run(app, host="0.0.0.0", port=8000)


if __name__ == "__main__":
    thread = Thread(target=run_server)
    thread.daemon = True
    thread.start()

    # Give the server a moment to start
    print("Waiting for server to start...")
    time.sleep(3)
    print("Sending test request...")

    response = requests.get("http://127.0.0.1:8000/my-shopping-cart-1/order-test")
    print(response.text)

    response = requests.get("http://127.0.0.1:8000/my-shopping-cart-1/order-test")
    print(response.text)

    # both invocations work (even if they were to be invoked in parallel)

Exception is not delegated to resource managers

Exception is not delegated to resource managers

If an exception happens while a resource is being used, the exception is not delegated to the context manager of the resource, thus preventing the manager to take the appropriate action.
This is especially important for cases where a DB session has to be committed / rolled back based on the outcome of the operations.

Example runnable snippet:

import time
from threading import Thread
from typing import AsyncIterator

import anydi
import requests
import uvicorn
from anydi import Container
from anydi.ext.fastapi import Inject
from anydi.ext.starlette.middleware import RequestScopedMiddleware
from fastapi import FastAPI
from starlette.middleware import Middleware


class SomePersistentResource:
    def use_resource(self):
        print("Resource being used!")

    def commit(self):
        print("Resource committed!")

    def rollback(self):
        print("Resource rolled back!")


container = Container()


@container.provider(scope="request")
async def create_resource() -> AsyncIterator[SomePersistentResource]:
    print("Initializing resource...")
    res = SomePersistentResource()
    try:
        yield res
    except Exception:
        # PROBLEM: this branch is not called when exception happens while using the resource
        res.rollback()
    else:
        # PROBLEM: this branch is called always
        res.commit()


app = FastAPI(middleware=[
    Middleware(RequestScopedMiddleware, container=container),
])


@app.get("/some-endpoint")
async def some_endpoint(res: SomePersistentResource = Inject()) -> str:
    res.use_resource()
    raise RuntimeError()
    return "Success"


anydi.ext.fastapi.install(app, container)


def run_server():
    uvicorn.run(app, host="0.0.0.0", port=8000)


if __name__ == "__main__":
    thread = Thread(target=run_server)
    thread.daemon = True
    thread.start()

    # Give the server a moment to start
    print("Waiting for server to start...")
    time.sleep(3)
    print("Sending test request...")

    response = requests.get("http://127.0.0.1:8000/some-endpoint")
    print(response.text)

Output:

Waiting for server to start...
INFO:     Started server process [23344]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Sending test request...
Initializing resource...
Resource being used!
Resource committed!
INFO:     127.0.0.1:24335 - "GET /some-endpoint HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
  + Exception Group Traceback (most recent call last):
<error truncated>

(Resource committed instead of rolled back.)

Default to a scope when none is provided in auto-registration mode

The code at:

return None

defaults to scope None when there are no sub-dependencies with defined scopes.

This (as far as I understand) enforces that a scope decorator or manual registration is always mandatory on types, otherwise automatic registration cannot happen, which (I think) invalidates a bit the automatic registration itself for types that do not have any sub-dependencies (i.e. they can be injected by themselves).

Would it be possible to default to a scope (e.g. transient) thus making automatic registration more seamless?

Running unit test in Python 3.9 fails

Running unit test in this project fails:

======================================= test session starts =======================================
platform darwin -- Python 3.9.7, pytest-8.2.2, pluggy-1.5.0
rootdir: /Users/Younghoon/projects/whova/anydi
configfile: pyproject.toml
plugins: anyio-4.4.0
collected 158 items / 2 errors                                                                    

============================================= ERRORS ==============================================
_______________________________ ERROR collecting tests/ext/fastapi ________________________________
../../../opt/anaconda3/lib/python3.9/importlib/__init__.py:127: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1030: in _gcd_import
    ???
<frozen importlib._bootstrap>:1007: in _find_and_load
    ???
<frozen importlib._bootstrap>:986: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:680: in _load_unlocked
    ???
../../../opt/anaconda3/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:178: in exec_module
    exec(co, module.__dict__)
tests/ext/fastapi/conftest.py:9: in <module>
    from .app import app as _app
tests/ext/fastapi/app.py:90: in <module>
    anydi.ext.fastapi.install(app, container)
anydi/ext/fastapi.py:50: in install
    for parameter in get_typed_parameters(call):
anydi/_utils.py:97: in get_typed_parameters
    return [
anydi/_utils.py:99: in <listcomp>
    annotation=get_typed_annotation(
anydi/_utils.py:75: in get_typed_annotation
    annotation = ForwardRef(annotation, module=module, is_class=is_class)
E   TypeError: __init__() got an unexpected keyword argument 'is_class'
________________________ ERROR collecting tests/ext/test_pytest_plugin.py _________________________
'inject' not found in `markers` configuration option
===================================== short test summary info =====================================
ERROR tests/ext/fastapi - TypeError: __init__() got an unexpected keyword argument 'is_class'
ERROR tests/ext/test_pytest_plugin.py - Failed: 'inject' not found in `markers` configuration option

It seems ForwardRef does not support is_class parameter in Python 3.9, but the if-statement for the python version does not include Python 3.9

https://github.com/antonrh/anydi/blob/main/anydi/_utils.py#L72

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.