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
@wireenables dependency resolution for a function or method.wired(factory)declares how a dependency is created.Wired()marks a reusableAnnotateddependency 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
WiremeErroris the base error exposed by Wireme.ValidationErrorrepresents 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
examples/basic.pycovers direct and reusable dependencies.examples/classes.pycovers constructor and method injection.examples/overrides.pycovers isolated test overrides.examples/protocols.pycovers interface-based dependencies.examples/resources.pycovers async resource cleanup.examples/custom_integration.pybuilds a typed service registry integration.
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, andWiredvocabulary. - Strong return typing for sync, async, generator, and async-generator factories.
- Reusable
Annotateddependencies. - Injected parameters hidden from public runtime signatures.
- Nested-safe dependency overrides.
- A minimal backend-independent public namespace.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2cf239a8d17057dd8753e7b8c6855c6e4afd03ab0df4b0bc03e3e1835a44948c
|
|
| MD5 |
ab81c0b865131b35ae28577fe8552338
|
|
| BLAKE2b-256 |
848921131d7aaac54e73e85db4ce338adbc02b4b911fb00272243ba57fe496eb
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
64875d49d3615fccc4b0a32fa16370862e86836542527fe49fdd1596b71e55be
|
|
| MD5 |
2cc48aa1216265ee778f45f97f014110
|
|
| BLAKE2b-256 |
1d572a267bc13febeba3ea1f6cfb385e5d5c03b7445504c558cee6160f18b875
|