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.2.2.tar.gz (7.4 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.2.2-py3-none-any.whl (10.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: io_adapters-0.2.2.tar.gz
  • Upload date:
  • Size: 7.4 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.2.2.tar.gz
Algorithm Hash digest
SHA256 e9507037cb1c3e96e5b56fa055c3c36bbfb4ccc226ca848236d17441c0f36c7e
MD5 0834d47dc50133dec75dc0076c733b0c
BLAKE2b-256 1cac43351192a306050a3c4cf7f96e5b95cfa16eac444b5cd6c1ec6d892fc5eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for io_adapters-0.2.2.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.2.2-py3-none-any.whl.

File metadata

  • Download URL: io_adapters-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 10.4 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.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f620ccc4a6e4cfd4f0b27884f75c02f4ea6cedacb2e056c1ed8856dcdcc3c756
MD5 d59bd163ae165ce34fbdf4683e125f33
BLAKE2b-256 0b57f07b8136df25c1d17e1b47333fbd45a0c14c6a263ce7b2d459e5cae0014e

See more details on using hashes here.

Provenance

The following attestation bundles were made for io_adapters-0.2.2-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