Skip to content

Commit

Permalink
explicit module protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
zkurtz committed Nov 30, 2024
1 parent ffb03af commit 74d3492
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 50 deletions.
35 changes: 15 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,23 @@ jobs:
- '3.13'

steps:
- name: Clone repo
uses: actions/checkout@v4
- name: Clone repo
uses: actions/checkout@v4

- name: Set the python version
run: echo "UV_PYTHON=${{ matrix.python-version }}" >> $GITHUB_ENV
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v4
with:
version: "0.5.4"
python-version: ${{ matrix.python-version }}

- name: Setup uv
uses: astral-sh/setup-uv@v3
with:
version: "0.5.4"
- name: Linting check
run: uv run ruff check

- name: Install extras
run: uv sync --group extras
- name: Formatting check
run: uv run ruff format --check

- name: Linting check
run: uv run ruff check
- name: Type checking
run: uv run pyright

- name: Formatting check
run: uv run ruff format --check

- name: Type checking
run: uv run pyright

- name: Unit tests
run: uv run pytest
- name: Unit tests
run: uv run pytest
43 changes: 43 additions & 0 deletions dummio/protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Runtime validation of the dummio module protocol."""

from types import ModuleType
from typing import Callable, get_type_hints

from dummio.constants import PathType


def assert_module_protocol(module: ModuleType) -> None:
"""Assert that a module implements save and load in a consistent way."""
if not hasattr(module, "save"):
raise AttributeError("Module is missing 'save' attribute")
if not hasattr(module, "load"):
raise AttributeError("Module is missing 'load' attribute")

# make the following assertions about the save attribute:
# - it is a function
# - the first argument is named "data"
# - all subsequent arguments are keyword-only
# - the second argument is "filepath" of type dummio.constants.PathType
if not isinstance(module.save, Callable):
raise TypeError("'save' attribute is not callable")
signature = get_type_hints(module.save)
first_two_args = list(signature.keys())[:2]
if first_two_args != ["data", "filepath"]:
raise TypeError("First two arguments of 'save' must be 'data' and 'filepath'")
if signature["filepath"] != PathType:
raise TypeError("'filepath' argument of 'save' must be of type PathType")

# make the following assertions about the load attribute:
# - it is a function
# - the first argument is named "filepath", of type dummio.constants.PathType
# - the return type is the same as the "data" argument of the save function
if not isinstance(module.load, Callable):
raise TypeError("'load' attribute is not callable")
signature = get_type_hints(module.load)
first_arg = list(signature.keys())[0]
if first_arg != "filepath":
raise TypeError("First argument of 'load' must be 'filepath'")
if signature["filepath"] != PathType:
raise TypeError("'filepath' argument of 'load' must be of type PathType")
if signature["return"] != get_type_hints(module.save)["data"]:
raise TypeError("Return type of 'load' must match 'data' argument type of 'save'")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "dummio"
version = "1.2.0"
version = "1.3.0"
description = "Easiest-possible IO for basic file types."
authors = [{ name = "Zach Kurtz", email = "[email protected]" }]
readme = "README.md"
Expand Down
37 changes: 8 additions & 29 deletions tests/test_assert_module_protocol.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Assert that every IO module implements save and load in a consistent way."""

import importlib
from typing import Callable, get_type_hints

from dummio.constants import PathType
import pytest

from dummio.protocol import assert_module_protocol

IO_MODULES = [
"dummio.json",
Expand All @@ -16,30 +17,8 @@
]


def test_assert_module_protocol() -> None:
for module_name in IO_MODULES:
module = importlib.import_module(module_name)
assert hasattr(module, "save")
assert hasattr(module, "load")

# make the following assertions about the save attribute:
# - it is a function
# - the first argument is named "data"
# - all subsequent arguments are keyword-only
# - the second argument is "filepath" of type dummio.constants.PathType
assert isinstance(module.save, Callable)
signature = get_type_hints(module.save)
first_two_args = list(signature.keys())[:2]
assert first_two_args == ["data", "filepath"]
assert signature["filepath"] == PathType

# make the following assertions about the load attribute:
# - it is a function
# - the first argument is named "filepath", of type dummio.constants.PathType
# - the return type is the same as the "data" argument of the save function
assert isinstance(module.load, Callable)
signature = get_type_hints(module.load)
first_arg = list(signature.keys())[0]
assert first_arg == "filepath"
assert signature["filepath"] == PathType
assert signature["return"] == get_type_hints(module.save)["data"]
# decorate the test function with pytest.mark.parametrize
@pytest.mark.parametrize("module_path", IO_MODULES)
def test_assert_module_protocol(module_path: str) -> None:
module = importlib.import_module(module_path)
assert_module_protocol(module)

0 comments on commit 74d3492

Please sign in to comment.