From 6f40119c7ec481559317eb8b573218e7b0810c7b Mon Sep 17 00:00:00 2001 From: Zach Kurtz Date: Sat, 30 Nov 2024 10:20:11 -0500 Subject: [PATCH] explicit module protocol --- .github/workflows/ci.yml | 36 +++++++++++------------ dummio/protocol.py | 43 ++++++++++++++++++++++++++++ pyproject.toml | 2 +- tests/test_assert_module_protocol.py | 37 ++++++------------------ 4 files changed, 69 insertions(+), 49 deletions(-) create mode 100644 dummio/protocol.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb9fe7b..483e0c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,28 +15,26 @@ 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: Install extras + run: uv sync --group extras - - name: Install extras - run: uv sync --group extras + - name: Linting check + run: uv run ruff check - - name: Linting check - run: uv run ruff check + - name: Formatting check + run: uv run ruff format --check - - name: Formatting check - run: uv run ruff format --check + - name: Type checking + run: uv run pyright - - name: Type checking - run: uv run pyright - - - name: Unit tests - run: uv run pytest + - name: Unit tests + run: uv run pytest diff --git a/dummio/protocol.py b/dummio/protocol.py new file mode 100644 index 0000000..53546bb --- /dev/null +++ b/dummio/protocol.py @@ -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'") diff --git a/pyproject.toml b/pyproject.toml index 18ee967..060d49d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "zkurtz@gmail.com" }] readme = "README.md" diff --git a/tests/test_assert_module_protocol.py b/tests/test_assert_module_protocol.py index 8ee2979..35d306d 100644 --- a/tests/test_assert_module_protocol.py +++ b/tests/test_assert_module_protocol.py @@ -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", @@ -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)