Configurable dependency(factory)

Configurable dependencies are simple functions that produce dependencies. They should be used to create dependencies with different behavior based on the arguments passed to them.

from collections.abc import Mapping

from fundi import from_, scan, inject, configurable_dependency, Scope


def require_user() -> Mapping[str, str | tuple[str]]:
    return {"username": "Kuyugama", "permissions": ("catch-apple",)}


@configurable_dependency
def require_permission(permission: str):
    def checker(user: Mapping[str, str | tuple[str]] = from_(require_user)) -> None:
        if permission not in user["permissions"]:
            raise PermissionError(permission)

    return checker


def application(
    _=from_(require_permission("catch-apple")),
):
    print("User has permission")


inject(Scope(), scan(application))

Note: @configurable_dependency is optional, but it caches dependencies, so their results can be cached on injection.

Also, @configurable_dependency does not cache dependencies configured with mutable arguments.

Composite dependencies

Composite dependencies are a special kind of configurable dependency that accept other dependencies as parameters.

These are a kind of higher-order functions, but with DI.

import typing
from collections.abc import Mapping

from fundi import from_, scan, inject, configurable_dependency, Scope


def require_user() -> dict[str, str | tuple[str]]:
    return {"username": "Kuyugama", "permissions": ("catch-apple",)}


@configurable_dependency
def require_permission(
    permission: str,
    user_resolver: typing.Callable[..., Mapping[str, str | tuple[str]]] = require_user,
):
    def checker(user: Mapping[str, str | tuple[str]] = from_(user_resolver)) -> None:
        if permission not in user["permissions"]:
            raise PermissionError(permission)

    return checker


def application(
    _=from_(require_permission("catch-apple")),
):
    print("User has permission")


inject(Scope(), scan(application))

Dependency configuration

When a configurable dependency is called, FunDI stores its configuration, so third-party tools (e.g. routers, docs generators, validators) can extract the metadata.

To get configuration of already scanned(using fundi.scan.scan) dependency — you can use CallableInfo.configuration attribute

If dependency is not scanned — use is_configured(call) function to check whether dependency is configured:

from fundi import is_configured, configurable_dependency


@configurable_dependency
def auth(optional: bool = False):
    return lambda: optional


assert is_configured(auth())

And to get dependency configuration use get_configuration(call) function on dependency callable:

import inspect

from fundi import get_configuration, configurable_dependency


@configurable_dependency
def auth(optional: bool = False):
    return lambda: optional


config = get_configuration(auth())

origin = inspect.unwrap(auth)

assert config.configurator.call == origin
assert config.values == {"optional": False}