Skip to main content

Dependency Injection Adapters

Project description

io-adapters

A small utility library for decoupling I/O from business logic by combining dependency injection with lightweight, automatically generated fakes.

Install

uv add io-adapters

API Reference

io-adapters API docs

Testing use cases that involve I/O is inherently difficult because they depend on:

  • external state (filesystems, databases, services)

  • side effects that are hard to observe directly

  • slow or flaky infrastructure

A common mitigation is to combine:

  • Dependency Injection (DI)

  • The Repository / Adapter pattern

This allows business logic to depend on an abstract interface rather than concrete I/O.

However, in practice this usually requires:

  • writing and maintaining bespoke fake implementations

  • keeping fake behaviour in sync with real implementations

  • duplicating boilerplate across domains

For small or medium-sized projects, this overhead can outweigh the benefits.

Simply register each I/O function with one of the register decorators and the functionality will be added to the RealAdapter object, on top of that a stub will be added to the FakeAdapter object too so you can pass in either to your usecase and the functionality will work.

Example

from enum import Enum
from pathlib import Path

from io_adapters import (
    IoAdapter,
    RealAdapter,
    add_domain,
    get_fake_adapter,
    get_real_adapter,
    register_domain_read_fn,
    register_domain_write_fn,
)


# you can use any hashable object to register an I/O function
class FileFormat(Enum):
    JSON = "json"


add_domain("orders")
add_domain("payment")


@register_domain_read_fn("orders", "str")
def read_str(path: str | Path, **kwargs: dict) -> str: ...


# stack decorators to register the same function to multiple domains
@register_domain_read_fn("orders", FileFormat.JSON)
@register_domain_read_fn("payment", FileFormat.JSON)
def read_json(path: str | Path, **kwargs: dict) -> dict: ...


@register_domain_write_fn("orders", "str")
def write_str(data: dict, path: str | Path, **kwargs: dict) -> None: ...


@register_domain_write_fn("orders", FileFormat.JSON)
@register_domain_write_fn("payment", FileFormat.JSON)
def write_json(data: dict, path: str | Path, **kwargs: dict) -> None: ...


def some_usecase(adapter: IoAdapter, path: str) -> None:
    adapter.read(path, "str")
    # Some business logic
    new_path = f"{path}_new.json"

    adapter.write({"a": 1}, new_path, FileFormat.JSON)


# in production inject the real adapter
orders_adapter: RealAdapter = get_real_adapter("orders")
some_usecase(orders_adapter, "some/path/to/file.json")


# in testing inject the fake which has all the same funcitonality as the
# `RealAdapter` and assert that the fakes end state is as expected
fake = get_fake_adapter("orders")
some_usecase(fake, "some/path/to/file.json")
assert fake.files["some/path/to/file.json"] == {"a": 1}

Repo map

├── .github
│   └── workflows
│       ├── ci_tests.yaml
│       └── publish.yaml
├── .pytest_cache
│   └── README.md
├── docs
│   └── source
│       └── conf.py
├── src
│   └── io_adapters
│       ├── __init__.py
│       ├── _adapters.py
│       ├── _clock.py
│       ├── _container.py
│       ├── _io_funcs.py
│       └── _registries.py
├── tests
│   ├── __init__.py
│   ├── test_adapters.py
│   ├── test_adapters_apis.py
│   └── test_container.py
├── .pre-commit-config.yaml
├── README.md
├── pyproject.toml
├── ruff.toml
└── uv.lock
::

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

io_adapters-0.3.0.tar.gz (7.8 kB view details)

Uploaded Source

Built Distribution

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

io_adapters-0.3.0-py3-none-any.whl (10.9 kB view details)

Uploaded Python 3

File details

Details for the file io_adapters-0.3.0.tar.gz.

File metadata

  • Download URL: io_adapters-0.3.0.tar.gz
  • Upload date:
  • Size: 7.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for io_adapters-0.3.0.tar.gz
Algorithm Hash digest
SHA256 dff386c4c7ffb751bbb1c1383c15c8669e611897ef5c349407f031126631908f
MD5 a9fec03f616b0f57d306596e654a23b9
BLAKE2b-256 c01409f35b8b695b3a9bb973218568d0e29d9331886163928760fa5c62364b44

See more details on using hashes here.

Provenance

The following attestation bundles were made for io_adapters-0.3.0.tar.gz:

Publisher: publish.yaml on second-ed/io-adapters

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file io_adapters-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: io_adapters-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 10.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for io_adapters-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 598be971e748ffffd94b538a8aa879ecc45f0486fa46d4a6cf9a004621e179e7
MD5 e9b3b908dfd5f63444fa49c41d1d0189
BLAKE2b-256 5bf5c0dfbfc5c62d4729aefa1bb61c9505e9a606465f887ff96a2949711c4cbc

See more details on using hashes here.

Provenance

The following attestation bundles were made for io_adapters-0.3.0-py3-none-any.whl:

Publisher: publish.yaml on second-ed/io-adapters

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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