Skip to main content

Dependency Injection for FP

Project description

Partial Injector

A dependency injection container for Python designed around functional programming. It uses functools.partial to wire dependencies into functions, making it a natural fit for FP-style code where services are plain functions rather than class instances.

Requirements

Python 3.14+

Installation

pip install partial-injector

Quick Start

from partial_injector.partial_container import Container

def get_multiplier() -> int:
    return 6

def get_answer(get_multiplier: callable) -> int:
    return get_multiplier() * 7

container = Container()
container.register_singleton(get_multiplier)
container.register_singleton(get_answer)
container.build()

answer = container.resolve(get_answer)
print(answer())  # 42

The container inspects get_answer's parameter annotations, finds get_multiplier is registered, and creates a partial that pre-fills it.

Core Concepts

Registration Keys

Every registration has a key used to look it up later. By default the registered object itself (function, class, or instance) is the key. Pass key= to use something else — a type, a string, or a TypeAlias.

container.register_singleton(42, key=int)
container.register_singleton("hello", key="greeting")

Registration Types

Method Behaviour
register_singleton Built once at build() time; every resolve() returns the same object.
register_transient A fresh deep copy (or new partial) is produced on each resolve().
register_singleton_factory Calls a factory at build() time; result is cached like a singleton.
register_transient_factory Calls the factory on every resolve().

Build and Resolve

container.build()          # wire all dependencies; no new registrations allowed after this
value = container.resolve(key)

Dependency Injection into Functions

The container resolves function parameters by matching each parameter's type annotation (or parameter name) against registered keys:

from typing import Callable

type Greeter = Callable[[str], str]

def make_greeter(prefix: str) -> Greeter:
    def greet(name: str) -> str:
        return f"{prefix}, {name}!"
    return greet

container = Container()
container.register_singleton("Hello", key=str)
container.register_singleton_factory(make_greeter, key=Greeter)
container.build()

greeter = container.resolve(Greeter)
print(greeter("World"))  # Hello, World!

Parameters that cannot be resolved are left unbound — they become arguments the caller supplies.

FromContainer

FromContainer lets one registration derive its value from another, optionally via a selector lambda:

from partial_injector.partial_container import Container, FromContainer
from dataclasses import dataclass

@dataclass
class Config:
    db_url: str
    debug: bool

container = Container()
container.register_singleton(Config(db_url="postgresql://localhost/mydb", debug=True), key=Config)
container.register_singleton(FromContainer(Config, lambda c: c.db_url), key="db_url")
container.register_singleton(FromContainer(Config, lambda c: c.debug), key="debug")
container.build()

db_url = container.resolve("db_url")  # "postgresql://localhost/mydb"

FromContainer can also be passed as a factory_args or factory_kwargs value, and it will be resolved at that point.

Multiple Registrations for the Same Key

Registering more than one object under the same key groups them automatically. Resolve the group as list[Key]:

from typing import Callable

type Validator = Callable[[str], bool]

def not_empty(value: str) -> bool:
    return len(value) > 0

def no_spaces(value: str) -> bool:
    return " " not in value

container = Container()
container.register_singleton(not_empty, key=Validator)
container.register_singleton(no_spaces, key=Validator)
container.build()

validators = container.resolve(list[Validator])
all_pass = all(v("hello") for v in validators)  # True

If only one of several registrations survives its condition, it can still be resolved as a single item by the bare key.

Conditional Registrations

Every register_* method accepts condition, condition_args, condition_kwargs, and throw_if_condition_not_satisfied. The condition is a callable that returns bool; if it returns False the registration is skipped.

import os

container = Container()
container.register_singleton(
    RealDatabase(),
    key="db",
    condition=lambda: os.getenv("ENV") == "production",
)
container.register_singleton(
    FakeDatabase(),
    key="db",
    condition=lambda: os.getenv("ENV") != "production",
)
container.build()

Set throw_if_condition_not_satisfied=True to raise PartialContainerException when no registration for a key passes its condition.

For TRANSIENT and TRANSIENT_FACTORY registrations the condition is evaluated lazily at each resolve() call rather than at build() time.

inject_returns

When a registered function returns another function, set inject_returns=True to wire that returned function's dependencies too:

from typing import Callable

type Handler = Callable[[str], str]
type HandlerFactory = Callable[[], Handler]

def make_handler(logger: Callable[[str], None]) -> Handler:
    def handle(message: str) -> str:
        logger(message)
        return f"handled: {message}"
    return handle

container = Container()
container.register_singleton(print, key=Callable[[str], None])
container.register_singleton(make_handler, key=HandlerFactory, inject_returns=True)
container.build()

factory = container.resolve(HandlerFactory)
handler = factory()           # returns handle with logger already injected
handler("ping")               # prints "ping", returns "handled: ping"

This works recursively and supports async functions.

inject_items

Register a list of objects and set inject_items=True to have each element processed through the container's injection logic individually:

type Processor = Callable[[int], int]

def double(factor: int, value: int) -> int:
    return value * factor

container = Container()
container.register_singleton(2, key=int)
container.register_singleton([double, double], key=list[Processor], inject_items=True)
container.build()

processors = container.resolve(list[Processor])
results = [p(3) for p in processors]  # [6, 6]

Error Handling

All errors raise PartialContainerException from partial_injector.error_handling.

Situation Message
Register after build() "Container already built"
resolve() before build() "Container not built"
Key not registered "Object with key <key> not registered"
Key not built (condition failed, no throw) key absent from resolved result
Key not built, throw_if_condition_not_satisfied=True "No object with key <key> was built because the built condition has not been met."
Unresolvable parameter before a resolvable one "Cannot build partial function without registered parameter <name>:<type>"

License

MIT

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

partial_injector-3.3.0.tar.gz (12.2 kB view details)

Uploaded Source

Built Distribution

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

partial_injector-3.3.0-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

File details

Details for the file partial_injector-3.3.0.tar.gz.

File metadata

  • Download URL: partial_injector-3.3.0.tar.gz
  • Upload date:
  • Size: 12.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for partial_injector-3.3.0.tar.gz
Algorithm Hash digest
SHA256 e1f9ab2c3415d7cceaa9caff9dcb091161c43438e7c7ea86e92ae008b8a9f315
MD5 2d3c8571d0971a8a53de6b202e769b7e
BLAKE2b-256 b079849375b8337483979322ad81532e0e458f122ae5aebb8292c437170b8f32

See more details on using hashes here.

File details

Details for the file partial_injector-3.3.0-py3-none-any.whl.

File metadata

  • Download URL: partial_injector-3.3.0-py3-none-any.whl
  • Upload date:
  • Size: 9.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for partial_injector-3.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 11bf5b8d1e778ae7843c35d325483f62b4510f5601c2d3898c250cb7c9e3d6d4
MD5 60a0837b9fb53b249a4630145f358183
BLAKE2b-256 25eac1f5915b108b66b0bd52692606057129d4628e4717145a56d9476c14fdda

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