Add runtime-checkable Protocol typing to all public interfaces.
PR Title Check / check-title (pull_request) Successful in 7s
Test Python Package / unit-tests (pull_request) Successful in 9s
Code Quality Pipeline / code-quality (pull_request) Successful in 22s
Test Python Package / integration-tests (pull_request) Successful in 24s
Test Python Package / coverage-report (pull_request) Successful in 8s
PR Title Check / check-title (pull_request) Successful in 7s
Test Python Package / unit-tests (pull_request) Successful in 9s
Code Quality Pipeline / code-quality (pull_request) Successful in 22s
Test Python Package / integration-tests (pull_request) Successful in 24s
Test Python Package / coverage-report (pull_request) Successful in 8s
Enables structural subtyping for consumers while preserving nominal adapter inheritance, instantiation guards, and scan_keys defaults. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
0829354cc4
commit
f6ee9a3724
@@ -12,6 +12,8 @@ Subclass an adapter in your own repository to add domain-specific methods while
|
|||||||
| **Adapters** | Technology-specific base classes (`RedisAdapter`, `MinioAdapter`) |
|
| **Adapters** | Technology-specific base classes (`RedisAdapter`, `MinioAdapter`) |
|
||||||
| **Your project** | Subclass an adapter and add domain methods |
|
| **Your project** | Subclass an adapter and add domain methods |
|
||||||
|
|
||||||
|
Each public interface is a `@runtime_checkable` `Protocol` with `@abstractmethod` members. **Subclass an adapter** when you need connection management and shared behavior — explicit subclasses get runtime instantiation guards and inherited default methods (e.g. `scan_keys`). **Type-annotate against an interface** when you want loose coupling — any object with the right methods satisfies the contract for mypy and `isinstance()` checks, without inheriting from this package.
|
||||||
|
|
||||||
Connection adapters expose `connect()`, `disconnect()`, and `is_connected()`. The latter verifies backend reachability with a cached health probe (default TTL: 1 second). Subclasses may override `health_check_ttl_seconds`. `connect()` is idempotent: calling it while already connected and healthy is a no-op.
|
Connection adapters expose `connect()`, `disconnect()`, and `is_connected()`. The latter verifies backend reachability with a cached health probe (default TTL: 1 second). Subclasses may override `health_check_ttl_seconds`. `connect()` is idempotent: calling it while already connected and healthy is a no-op.
|
||||||
|
|
||||||
## Future direction
|
## Future direction
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ except ImportError as exc:
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
class MinioAdapter(ConnectionAwareAdapter, ObjectRepositoryInterface):
|
||||||
"""Minio adapter exposing basic CRUD functionality."""
|
"""Minio adapter exposing basic CRUD functionality."""
|
||||||
|
|
||||||
endpoint_env_var_name: str = "MINIO_ENDPOINT"
|
endpoint_env_var_name: str = "MINIO_ENDPOINT"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ except ImportError as exc:
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
class RedisAdapter(ConnectionAwareAdapter, JsonRepositoryInterface):
|
||||||
"""Redis adapter exposing basic CRUD functionality."""
|
"""Redis adapter exposing basic CRUD functionality."""
|
||||||
|
|
||||||
uri_env_var_name: str = "REDIS_URI"
|
uri_env_var_name: str = "REDIS_URI"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Definition of ConnectionAwareInterface abstract base class."""
|
"""Definition of ConnectionAwareInterface protocol and abstract base class."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import abstractmethod
|
||||||
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
class ConnectionAwareInterface(ABC):
|
@runtime_checkable
|
||||||
|
class ConnectionAwareInterface(Protocol):
|
||||||
"""Interface that defines connection-related methods."""
|
"""Interface that defines connection-related methods."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"""Definition of ContextAwareInterface abstract base class."""
|
"""Definition of ContextAwareInterface protocol and abstract base class."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import Self
|
from typing import Protocol, Self, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
class ContextAwareInterface(ABC):
|
@runtime_checkable
|
||||||
|
class ContextAwareInterface(Protocol):
|
||||||
"""Interface that defines context-related methods."""
|
"""Interface that defines context-related methods."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""Definition of JsonRepositoryInterface abstract base class."""
|
"""Definition of JsonRepositoryInterface protocol and abstract base class."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import abstractmethod
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from typing import Any
|
from typing import Any, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
class JsonRepositoryInterface(ABC):
|
@runtime_checkable
|
||||||
|
class JsonRepositoryInterface(Protocol):
|
||||||
"""Interface that defines JSON document CRUD methods."""
|
"""Interface that defines JSON document CRUD methods."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""Definition of ObjectRepositoryInterface abstract base class."""
|
"""Definition of ObjectRepositoryInterface protocol and abstract base class."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import abstractmethod
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
class ObjectRepositoryInterface(ABC):
|
@runtime_checkable
|
||||||
|
class ObjectRepositoryInterface(Protocol):
|
||||||
"""Interface that defines binary object CRUD methods."""
|
"""Interface that defines binary object CRUD methods."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Unit tests for structural typing of public interfaces."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from python_repositories.interfaces import (
|
||||||
|
ConnectionAwareInterface,
|
||||||
|
ContextAwareInterface,
|
||||||
|
JsonRepositoryInterface,
|
||||||
|
ObjectRepositoryInterface,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryJsonRepo:
|
||||||
|
"""Plain class that satisfies JsonRepositoryInterface without inheritance."""
|
||||||
|
|
||||||
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key: str, data: dict[str, Any]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_keys(self, pattern: str) -> list[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def scan_keys(
|
||||||
|
self,
|
||||||
|
pattern: str,
|
||||||
|
*,
|
||||||
|
count: int | None = None,
|
||||||
|
) -> Iterator[str]:
|
||||||
|
del count
|
||||||
|
yield from self.list_keys(pattern)
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryObjectRepo:
|
||||||
|
"""Plain class that satisfies ObjectRepositoryInterface without inheritance."""
|
||||||
|
|
||||||
|
def get(self, object_name: str) -> BytesIO | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def put(
|
||||||
|
self,
|
||||||
|
object_name: str,
|
||||||
|
data: BytesIO,
|
||||||
|
content_type: str = "application/octet-stream",
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self, object_name: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_objects(self, prefix: str = "") -> list[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConnection:
|
||||||
|
"""Plain class that satisfies ConnectionAwareInterface without inheritance."""
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FakeContextManager:
|
||||||
|
"""Plain class that satisfies ContextAwareInterface without inheritance."""
|
||||||
|
|
||||||
|
def __enter__(self) -> FakeContextManager:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def accepts_json_repo(repo: JsonRepositoryInterface) -> None:
|
||||||
|
"""Type-checking hook for JsonRepositoryInterface structural subtyping."""
|
||||||
|
repo.get("key")
|
||||||
|
|
||||||
|
|
||||||
|
def accepts_object_repo(repo: ObjectRepositoryInterface) -> None:
|
||||||
|
"""Type-checking hook for ObjectRepositoryInterface structural subtyping."""
|
||||||
|
repo.get("object")
|
||||||
|
|
||||||
|
|
||||||
|
def accepts_connection_aware(connection: ConnectionAwareInterface) -> None:
|
||||||
|
"""Type-checking hook for ConnectionAwareInterface structural subtyping."""
|
||||||
|
connection.is_connected()
|
||||||
|
|
||||||
|
|
||||||
|
def accepts_context_aware(context: ContextAwareInterface) -> None:
|
||||||
|
"""Type-checking hook for ContextAwareInterface structural subtyping."""
|
||||||
|
with context:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_repository_structural_subtyping() -> None:
|
||||||
|
"""Test that a plain class satisfies JsonRepositoryInterface structurally."""
|
||||||
|
repo: JsonRepositoryInterface = InMemoryJsonRepo()
|
||||||
|
accepts_json_repo(repo)
|
||||||
|
assert isinstance(repo, JsonRepositoryInterface)
|
||||||
|
|
||||||
|
|
||||||
|
def test_object_repository_structural_subtyping() -> None:
|
||||||
|
"""Test that a plain class satisfies ObjectRepositoryInterface structurally."""
|
||||||
|
repo: ObjectRepositoryInterface = InMemoryObjectRepo()
|
||||||
|
accepts_object_repo(repo)
|
||||||
|
assert isinstance(repo, ObjectRepositoryInterface)
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_aware_structural_subtyping() -> None:
|
||||||
|
"""Test that a plain class satisfies ConnectionAwareInterface structurally."""
|
||||||
|
connection: ConnectionAwareInterface = FakeConnection()
|
||||||
|
accepts_connection_aware(connection)
|
||||||
|
assert isinstance(connection, ConnectionAwareInterface)
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_aware_structural_subtyping() -> None:
|
||||||
|
"""Test that a plain class satisfies ContextAwareInterface structurally."""
|
||||||
|
context: ContextAwareInterface = FakeContextManager()
|
||||||
|
accepts_context_aware(context)
|
||||||
|
assert isinstance(context, ContextAwareInterface)
|
||||||
Reference in New Issue
Block a user