Skip to main content

Tiny, typed dependency injection built on FastDepends.

Project description

wireme

Tiny, typed dependency injection for Python, powered by FastDepends.

Wireme keeps dependency injection explicit and small:

from wireme import Wired, override_dependency, wire, wired
  • @wire enables dependency resolution for a function or method.
  • wired(factory) declares how a dependency is created.
  • Wired() marks a reusable Annotated dependency as caller-optional.
  • override_dependency() temporarily replaces a dependency in tests.

Installation

uv add wireme

Wireme requires Python 3.12 or newer.

Quick start

from wireme import wire, wired


class Database:
    def write(self, value: str) -> None:
        print(f"writing: {value}")


def get_database() -> Database:
    return Database()


@wire
def process_text(
    text: str,
    database: Database = wired(get_database),
) -> None:
    database.write(text)


process_text("Hello, world")

Callers only provide application inputs. Wireme resolves database automatically. Injected parameters are also removed from the public runtime signature:

import inspect

assert str(inspect.signature(process_text)) == "(text: str) -> None"

Reusable dependencies

Use Annotated when the same dependency appears in multiple callables:

from typing import Annotated

from wireme import Wired, wire, wired


type DatabaseDep = Annotated[Database, wired(get_database)]


@wire
def create_user(
    username: str,
    database: DatabaseDep = Wired(),
) -> None:
    database.write(username)


create_user("mo")

wired(get_database) stores the dependency declaration in the annotation. Wired() tells type checkers and call sites that the argument does not need to be passed explicitly.

Wiring classes

Decorate constructors or methods individually. Do not decorate the class itself.

Use constructor injection when the dependency belongs to the object's state:

from typing import Annotated

from wireme import Wired, wire, wired


type DatabaseDep = Annotated[Database, wired(get_database)]


class TextProcessor:
    @wire
    def __init__(self, database: DatabaseDep = Wired()) -> None:
        self._database = database

    def process(self, text: str) -> None:
        self._database.write(text)


TextProcessor().process("Hello from constructor injection")

Use method injection when only a specific operation needs the dependency:

class TextProcessor:
    @wire
    def process(
        self,
        text: str,
        database: DatabaseDep = Wired(),
    ) -> None:
        database.write(text)


TextProcessor().process("Hello from method injection")

Constructor injection is useful for dependencies shared across multiple methods. Method injection keeps the object stateless when only one operation needs the dependency.

See examples/classes.py for a complete runnable example.

Explicit values take precedence

A caller may still provide a dependency explicitly. The passed value wins over injection:

class TestDatabase(Database):
    pass


create_user("mo", TestDatabase())

This is useful for one-off composition. For test suites, prefer override_dependency() so every nested dependency sees the replacement.

Test overrides

from wireme import override_dependency


def get_test_database() -> Database:
    return TestDatabase()


with override_dependency(get_database, get_test_database):
    create_user("mo")

Overrides are restored when the context exits, including after exceptions. Nested overrides restore the previous outer override correctly.

The provider is shared at process level. Use overrides for isolated tests and application setup, not concurrent request-level mutation.

Nested dependencies and caching

Factories can depend on other factories:

from wireme import wire, wired


def get_settings() -> Settings:
    return Settings()


def get_database(
    settings: Settings = wired(get_settings),
) -> Database:
    return Database(settings.database_url)


@wire
def operation(database: Database = wired(get_database)) -> None:
    ...

Dependencies are cached once per wired call by default. Disable caching for a specific declaration when every use must create a new value:

value: Token = wired(create_token, use_cache=False)

Async and resource dependencies

Async factories are supported:

async def get_client() -> Client:
    return await Client.connect()


@wire
async def fetch_user(
    user_id: str,
    client: Client = wired(get_client),
) -> User:
    return await client.fetch_user(user_id)

Generator and async-generator factories can own resource cleanup:

from collections.abc import AsyncIterator


async def get_client() -> AsyncIterator[Client]:
    client = await Client.connect()
    try:
        yield client
    finally:
        await client.close()

Cleanup runs after the wired callable finishes.

Protocol dependencies

A protocol can describe the dependency interface. When runtime validation is active, make the protocol runtime-checkable:

from typing import Annotated, Protocol, runtime_checkable

from wireme import Wired, wire, wired


@runtime_checkable
class DatabaseLike(Protocol):
    def write(self, value: str) -> None: ...


type DatabaseDep = Annotated[DatabaseLike, wired(get_database)]


@wire
def process(
    value: str,
    database: DatabaseDep = Wired(),
) -> None:
    database.write(value)

Build integrations on top of Wireme

Wireme can be the small DI primitive behind a project-specific API. For example, a service or plugin registry can expose wired_service(ServiceType) while still using Wireme for resolution, typing, caching, and overrides.

import functools

from collections.abc import Callable
from typing import cast

from wireme import WiremeError, wired


class ServiceUnavailableError(WiremeError):
    pass


_services: dict[type[object], object] = {}


@functools.cache
def require_service[T](service_type: type[T]) -> Callable[[], T]:
    def dependency() -> T:
        try:
            service = _services[service_type]
        except KeyError as error:
            raise ServiceUnavailableError(
                f"{service_type.__name__} is not registered."
            ) from error

        return cast(T, service)

    return dependency


def wired_service[T](service_type: type[T]) -> T:
    return wired(require_service(service_type))

Caching the generated factory gives it stable identity, which is important when using override_dependency().

See examples/custom_integration.py for a complete runnable example.

Errors

Wireme exposes:

from wireme import ValidationError, WiremeError
  • WiremeError is the base error exposed by Wireme.
  • ValidationError represents dependency input or result validation failures.

Project-specific DI errors may inherit from WiremeError.

Ruff configuration

Ruff's B008 rule normally rejects function calls in defaults. Tell Ruff that Wired() is an immutable marker:

[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["wireme.Wired"]

Declarations using wired(factory) may also need your project's normal DI rule configuration, depending on which Ruff rules are enabled.

Public API

Name Purpose
wire Decorate a function or method and enable dependency resolution.
wired Declare a dependency factory and its resolution options.
Wired Mark an Annotated dependency as caller-optional.
override_dependency Temporarily replace a factory, with nested restoration.
WiremeError Base error exposed by Wireme.
ValidationError Dependency validation error.

Examples

Run any example with:

uv run python examples/basic.py

Why Wireme instead of importing FastDepends directly?

FastDepends provides the resolution engine. Wireme provides a deliberately small, opinionated facade with:

  • A cohesive wire, wired, and Wired vocabulary.
  • Strong return typing for sync, async, generator, and async-generator factories.
  • Reusable Annotated dependencies.
  • Injected parameters hidden from public runtime signatures.
  • Nested-safe dependency overrides.
  • A minimal backend-independent public namespace.

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

wireme-0.1.0.tar.gz (7.0 kB view details)

Uploaded Source

Built Distribution

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

wireme-0.1.0-py3-none-any.whl (8.2 kB view details)

Uploaded Python 3

File details

Details for the file wireme-0.1.0.tar.gz.

File metadata

  • Download URL: wireme-0.1.0.tar.gz
  • Upload date:
  • Size: 7.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for wireme-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2cf239a8d17057dd8753e7b8c6855c6e4afd03ab0df4b0bc03e3e1835a44948c
MD5 ab81c0b865131b35ae28577fe8552338
BLAKE2b-256 848921131d7aaac54e73e85db4ce338adbc02b4b911fb00272243ba57fe496eb

See more details on using hashes here.

File details

Details for the file wireme-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: wireme-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 8.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for wireme-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 64875d49d3615fccc4b0a32fa16370862e86836542527fe49fdd1596b71e55be
MD5 2cc48aa1216265ee778f45f97f014110
BLAKE2b-256 1d572a267bc13febeba3ea1f6cfb385e5d5c03b7445504c558cee6160f18b875

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