Skip to main content

Dependency injection library

Project description

# FunDI

Solution for problem no one had before

Fun stays for function(or for fun if you wish) and DI for Dependency Injection

This library provides fast(to write!) and convenient(to use!) Dependency Injection for functional programming on python.

Why?

This library was inspired by FastAPI's dependency injection. The reasons for its existence are simple:

  • A standalone dependency injection library. DI shouldn't be tied to a specific framework.
  • It simplifies code writing. Dependency injection reduces boilerplate and improves maintainability.
  • Lack of DI libraries for functional programming in Python. Or maybe I just didn't want to find one :3

How?

Main definitions

  • Scope - dependency injection scope (list of root values that will be set to parameters without dependency function)

    Example:

    from contextlib import ExitStack
    
    from fundi import inject, scan
    
    def dependant(value: str):
        print(value)
    
    
    with ExitStack() as stack:
      inject({"value": "Value that will be passed to dependency"}, scan(dependant), stack)
    
  • Dependant - Function that has dependencies (either function, or scope dependencies)

    Example:

    from contextlib import ExitStack
    
    from fundi import from_, inject, scan
    
    class Session:
        pass
    
    
    def string() -> str:
        return "string from dependency"
    
    
    def dependant(session: from_(Session), scope_value: str, dependency_value: str = from_(string)):
        pass
    
    
    with ExitStack() as stack:
        inject({"scope_value": "string from scope", "session": Session()}, scan(dependant), stack)
    
  • Dependency - Function that is used to resolve dependant's parameter value. Dependency can have its own dependencies.

    Example:

    from contextlib import ExitStack
    
    from fundi import from_, inject, scan
    
    class Session:
        pass
    
    
    def string() -> str:
        return "string from dependency"
    
    
    def dependency(session: from_(Session), scope_value: str, dependency_value: str = from_(string)):
        pass
    
    
    def dependant(value: None = from_(dependency)):
        pass
    
    
    with ExitStack() as stack:
        inject({"scope_value": "string from scope", "session": Session()}, scan(dependant), stack)
    

No more words, let's try!

Sync

from contextlib import ExitStack
from typing import Generator, Any

from fundi import from_, inject, scan

def require_database_session(database_url: str) -> Generator[str, Any]:
    print(f"Opened database session at {database_url = }")
    yield "database session"
    print("Closed database session")


def require_user(session: str = from_(require_database_session)) -> str:
    return "user"


def application(user: str = from_(require_user), session: str = from_(require_database_session)):
    print(f"Application started with {user = }")


with ExitStack() as stack:
    inject({"database_url": "postgresql://kuyugama:insecurepassword@localhost:5432/database"}, scan(application), stack)

Async

import asyncio
from contextlib import AsyncExitStack
from typing import AsyncGenerator, Any

from fundi import from_, ainject, scan

async def require_database_session(database_url: str) -> AsyncGenerator[str, Any]:
    print(f"Opened database session at {database_url = }")
    yield "database session"
    print("Closed database session")


async def require_user(session: str = from_(require_database_session)) -> str:
    return "user"


async def application(user: str = from_(require_user), session: str = from_(require_database_session)):
    print(f"Application started with {user = }")


async def main():
    async with AsyncExitStack() as stack:
        await ainject({"database_url": "postgresql://kuyugama:insecurepassword@localhost:5432/database"}, scan(application), stack)


asyncio.run(main())

Resolve scope dependencies by type

It's simple! Use from_ on type annotation

from contextlib import ExitStack

from fundi import from_, inject, scan

class Session:
    """Database session"""

def require_user(_: from_(Session)) -> str:
    return "user"


def application(session: from_(Session), user: str = from_(require_user)):
    print(f"Application started with {user = }")
    print(f"{session = }")


with ExitStack() as stack:
    inject({"db": Session()}, scan(application), stack)

Note: while resolving dependencies by type - parameter names doesn't really matter.

Overriding dependency results

As simple, as injecting!

import time
import random
from contextlib import ExitStack

from fundi import from_, inject, scan


def require_random_animal() -> str:
    time.sleep(3)  # simulate web request

    return random.choice(["cat", "dog", "chicken", "horse", "platypus", "cow"])


def application(animal: str = from_(require_random_animal)):
    print("Animal:", animal)


with ExitStack() as stack:
    inject({}, scan(application), stack, override={require_random_animal: "dog"})

Overriding dependencies

As easy as say "Kuyugama is the best"!

import time
import random
from contextlib import ExitStack

from fundi import from_, inject, scan


def require_random_animal() -> str:
    time.sleep(3)  # simulate web request

    return random.choice(["cat", "dog", "chicken", "horse", "platypus", "cow"])


def test_require_animal(animal: str) -> str:
    return animal


def application(animal: str = from_(require_random_animal)):
    print("Animal:", animal)


with ExitStack() as stack:
    inject(
        {"animal": "cockroach"},
        scan(application),
        stack,
        override={require_random_animal: scan(test_require_animal)}
    )

Component explanation:

  • fundi.from_ - Helps define dependencies.

    Use cases:

    • Tell resolver to resolve parameter value by its type, not name (ex: parameter: from_(Session))

    • Define dependency (ex: parameter: type = from_(dependency)).

      In this case it can be replaced with fundi.scan.scan (ex: parameter: type = scan(dependency))

  • fundi.scan - Scans function for dependencies. Returns CallableInfo object that is used by all functions that resolve dependencies.

  • fundi.order - returns order in which dependencies will be resolved

  • fundi.tree - returns dependency resolving tree

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

fundi-0.0.7.tar.gz (31.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fundi-0.0.7-py3-none-any.whl (32.7 kB view details)

Uploaded Python 3

File details

Details for the file fundi-0.0.7.tar.gz.

File metadata

  • Download URL: fundi-0.0.7.tar.gz
  • Upload date:
  • Size: 31.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.26

File hashes

Hashes for fundi-0.0.7.tar.gz
Algorithm Hash digest
SHA256 33433efe1b5d587999f27ceaa6d82b6893ec7ee867d002cd514b694094e26779
MD5 08b77dadccd04187a10d53429451ce1a
BLAKE2b-256 a8a9628ed7be65a96db76ba86795d511bcef47d51ce69364f162a44c44ed644d

See more details on using hashes here.

File details

Details for the file fundi-0.0.7-py3-none-any.whl.

File metadata

  • Download URL: fundi-0.0.7-py3-none-any.whl
  • Upload date:
  • Size: 32.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.26

File hashes

Hashes for fundi-0.0.7-py3-none-any.whl
Algorithm Hash digest
SHA256 bdf5040b8e8b8c3c80cd316fefbf71000a2026710b40ffb3bbbe15b0531e6ce4
MD5 bce3ae4fc11f47498e64fb68608ab869
BLAKE2b-256 0b8aac1e8ec33b73558d5b5295fd86640db10a59eb45f90339ed61e6a2fec6fc

See more details on using hashes here.

Supported by

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