From 44b15cb21ad84ccc399cd2c03710f4c7a54cc29d Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 11 Jul 2026 10:32:17 +0200 Subject: [PATCH] Move structural typing tests into interface unit test files. Align test layout with the one-test-file-per-interface convention by removing structural_typing_test.py. Co-authored-by: Cursor --- tests/unit/connection_aware_interface_test.py | 25 ++++ tests/unit/context_aware_interface_test.py | 25 ++++ tests/unit/json_repository_interface_test.py | 38 +++++ .../unit/object_repository_interface_test.py | 33 +++++ tests/unit/structural_typing_test.py | 134 ------------------ 5 files changed, 121 insertions(+), 134 deletions(-) delete mode 100644 tests/unit/structural_typing_test.py diff --git a/tests/unit/connection_aware_interface_test.py b/tests/unit/connection_aware_interface_test.py index 560260c..4a36303 100644 --- a/tests/unit/connection_aware_interface_test.py +++ b/tests/unit/connection_aware_interface_test.py @@ -7,6 +7,24 @@ from python_repositories.interfaces.connection_aware_interface import ( ) +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 + + +def accepts_connection_aware(connection: ConnectionAwareInterface) -> None: + """Type-checking hook for ConnectionAwareInterface structural subtyping.""" + connection.is_connected() + + def test_instantiation_fails_when_connect_not_implemented() -> None: """Test that instantiation fails if connect is not implemented.""" @@ -53,3 +71,10 @@ def test_instantiation_fails_when_is_connected_not_implemented() -> None: with pytest.raises(TypeError): _ = Incomplete() # type: ignore + + +def test_structural_subtyping() -> None: + """Test that a plain class satisfies ConnectionAwareInterface structurally.""" + connection: ConnectionAwareInterface = FakeConnection() + accepts_connection_aware(connection) + assert isinstance(connection, ConnectionAwareInterface) diff --git a/tests/unit/context_aware_interface_test.py b/tests/unit/context_aware_interface_test.py index 925f322..205dc41 100644 --- a/tests/unit/context_aware_interface_test.py +++ b/tests/unit/context_aware_interface_test.py @@ -7,6 +7,24 @@ import pytest from python_repositories.interfaces.context_aware_interface import ContextAwareInterface +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_context_aware(context: ContextAwareInterface) -> None: + """Type-checking hook for ContextAwareInterface structural subtyping.""" + with context: + pass + + def test_instantiation_fails_when_enter_not_implemented() -> None: """Test that instantiation fails if __enter__ is not implemented.""" @@ -33,3 +51,10 @@ def test_instantiation_fails_when_exit_not_implemented() -> None: with pytest.raises(TypeError): _ = Incomplete() # type: ignore + + +def test_structural_subtyping() -> None: + """Test that a plain class satisfies ContextAwareInterface structurally.""" + context: ContextAwareInterface = FakeContextManager() + accepts_context_aware(context) + assert isinstance(context, ContextAwareInterface) diff --git a/tests/unit/json_repository_interface_test.py b/tests/unit/json_repository_interface_test.py index 208b4f9..70ecf1c 100644 --- a/tests/unit/json_repository_interface_test.py +++ b/tests/unit/json_repository_interface_test.py @@ -1,5 +1,6 @@ """Unit tests for JsonRepositoryInterface.""" +from collections.abc import Iterator from typing import Any import pytest @@ -9,6 +10,36 @@ from python_repositories.interfaces.json_repository_interface import ( ) +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) + + +def accepts_json_repo(repo: JsonRepositoryInterface) -> None: + """Type-checking hook for JsonRepositoryInterface structural subtyping.""" + repo.get("key") + + def test_instantiation_fails_when_get_not_implemented() -> None: """Test that instantiation fails if get is not implemented.""" @@ -104,3 +135,10 @@ def test_scan_keys_defaults_to_list_keys() -> None: repository = Complete() assert list(repository.scan_keys("user")) == ["user-1", "user-2"] + + +def test_structural_subtyping() -> None: + """Test that a plain class satisfies JsonRepositoryInterface structurally.""" + repo: JsonRepositoryInterface = InMemoryJsonRepo() + accepts_json_repo(repo) + assert isinstance(repo, JsonRepositoryInterface) diff --git a/tests/unit/object_repository_interface_test.py b/tests/unit/object_repository_interface_test.py index 4cebd2d..3bb1ce3 100644 --- a/tests/unit/object_repository_interface_test.py +++ b/tests/unit/object_repository_interface_test.py @@ -9,6 +9,32 @@ from python_repositories.interfaces.object_repository_interface import ( ) +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 [] + + +def accepts_object_repo(repo: ObjectRepositoryInterface) -> None: + """Type-checking hook for ObjectRepositoryInterface structural subtyping.""" + repo.get("object") + + def test_instantiation_fails_when_get_not_implemented() -> None: """Test that instantiation fails if get is not implemented.""" @@ -98,3 +124,10 @@ def test_instantiation_fails_when_list_objects_not_implemented() -> None: with pytest.raises(TypeError): _ = Incomplete() # type: ignore + + +def test_structural_subtyping() -> None: + """Test that a plain class satisfies ObjectRepositoryInterface structurally.""" + repo: ObjectRepositoryInterface = InMemoryObjectRepo() + accepts_object_repo(repo) + assert isinstance(repo, ObjectRepositoryInterface) diff --git a/tests/unit/structural_typing_test.py b/tests/unit/structural_typing_test.py deleted file mode 100644 index ddbc5a6..0000000 --- a/tests/unit/structural_typing_test.py +++ /dev/null @@ -1,134 +0,0 @@ -"""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)