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.4.0.tar.gz (8.1 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.4.0-py3-none-any.whl (11.1 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for io_adapters-0.4.0.tar.gz
Algorithm Hash digest
SHA256 6e57af7590700316a137f4ee9ac6f4431c68d916ac1b0cb81d7535ea490237bc
MD5 d968a6a91d989431e2fb5a65c6bb876d
BLAKE2b-256 6c01dc301a66f555b9a667c65e9054557e13b6fc10ec31df2a9cf804a248c371

See more details on using hashes here.

Provenance

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

File metadata

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

File hashes

Hashes for io_adapters-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2e0ce36e9d87e9cd9504ab40cc613a564fba1973b5b5cecfac48f3bb3148a2cf
MD5 b14f3d69e50cf15e5fe488266f93df14
BLAKE2b-256 373fff8b2e683be8d920d3ac9f288d031aaa0b1acbba30c55fc713461c7cfa7f

See more details on using hashes here.

Provenance

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