diff --git a/Makefile b/Makefile index 7145a49..0573b57 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ DOCKER_CACHE_FILE := $(DOCKER_CACHE_DIR)/scylla-image.tar CERT_CACHE_DIR := $(MAKEFILE_PATH)/.cert-cache CERT_DIR := $(MAKEFILE_PATH)/tests/scylla -.PHONY: install verify build compile compile-test compile-demo test-unit test-integration test-integration-only test-demo test-demo-only test-all test lint lint-types lint-fix clean wait-for-alternator scylla-start scylla-stop scylla-kill scylla-rm docker-pull docker-cache-save docker-cache-load cert-cache-save cert-cache-load help +.PHONY: install verify build compile compile-test compile-demo test-unit test-integration test-integration-only test-demo test-demo-only test-all test lint lint-types typecheck lint-fix clean wait-for-alternator scylla-start scylla-stop scylla-kill scylla-rm docker-pull docker-cache-save docker-cache-load cert-cache-save cert-cache-load help # Default target help: @@ -27,6 +27,7 @@ help: @echo " test - Run all tests" @echo " lint - Run linters (ruff + mypy)" @echo " lint-types - Enforce type annotations with ruff" + @echo " typecheck - Run mypy type checks" @echo " lint-fix - Auto-fix linting issues with ruff" @echo " clean - Remove build artifacts" @echo " scylla-start - Start 3-node Scylla cluster via Docker" @@ -96,11 +97,10 @@ test: --cov=alternator --cov-report=xml --cov-report=term-missing # Run linters -lint: lint-types +lint: lint-types typecheck uv run ruff check alternator/ tests/ examples/ uv run ruff format --check alternator/ tests/ examples/ $(MAKE) compile-demo - uv run mypy alternator/ --strict @# Ensure every noqa/type: ignore has an explanation after it @if grep -rn --include='*.py' -P '#\s*(noqa|type:\s*ignore)(?::\s*\S+)?\s*$$' alternator/ tests/ examples/; then \ echo "ERROR: Found noqa/type: ignore comments without explanations. Add ' -- reason' after each."; \ @@ -110,6 +110,9 @@ lint: lint-types lint-types: uv run ruff check --select ANN alternator/ tests/ examples/ +typecheck: + uv run mypy alternator/ examples/ tests/unit/ --strict + # Auto-fix linting issues lint-fix: uv run ruff check --fix alternator/ tests/ examples/ diff --git a/README.md b/README.md index feec191..1609010 100644 --- a/README.md +++ b/README.md @@ -841,6 +841,9 @@ make test-unit # Run linting make lint +# Run mypy type checks +make typecheck + # Start local Scylla cluster for integration tests make scylla-start make test-integration diff --git a/alternator/core/compression.py b/alternator/core/compression.py index 047dca6..f4205d4 100644 --- a/alternator/core/compression.py +++ b/alternator/core/compression.py @@ -65,7 +65,7 @@ def compress_request( return compress_request -def _set_request_body(request: AWSPreparedRequest | AWSRequest, body: bytes) -> None: +def _set_request_body(request: object, body: bytes) -> None: """Set request body for both prepared and pre-signing request objects.""" mutable_request = cast(Any, request) if isinstance(request, AWSPreparedRequest): diff --git a/alternator/core/request.py b/alternator/core/request.py index a6815b1..9a598dc 100644 --- a/alternator/core/request.py +++ b/alternator/core/request.py @@ -20,7 +20,7 @@ def extract_operation_name(request: AWSPreparedRequest | AWSRequest) -> str: Returns: Operation name (e.g. "PutItem"), or empty string if not found """ - target = request.headers.get("X-Amz-Target", "") + target: str | bytes = request.headers.get("X-Amz-Target", "") if isinstance(target, bytes): target = target.decode("utf-8") return target.split(".")[-1] if "." in target else "" diff --git a/examples/async_demo.py b/examples/async_demo.py index fe8cadd..7171302 100644 --- a/examples/async_demo.py +++ b/examples/async_demo.py @@ -12,6 +12,7 @@ import logging import os import sys +from typing import Any, cast from botocore.exceptions import ClientError @@ -39,8 +40,8 @@ async def main() -> None: async with AsyncAlternatorClient(config) as client: # List existing tables print("Listing tables...") - response = await client.list_tables() - tables = response.get("TableNames", []) + list_response = await client.list_tables() + tables = list_response.get("TableNames", []) print(f"Found {len(tables)} tables: {tables}") # Create a test table if it doesn't exist @@ -95,7 +96,7 @@ async def main() -> None: responses = await asyncio.gather(*read_tasks) for i, response in enumerate(responses): - item = response.get("Item", {}) + item = cast(dict[str, Any], response.get("Item", {})) print(f" async_user_{i}: {item.get('data', {}).get('S', 'N/A')}") print("Async demo complete!") diff --git a/examples/compare_aws_sdk.py b/examples/compare_aws_sdk.py index 27e0639..166031d 100644 --- a/examples/compare_aws_sdk.py +++ b/examples/compare_aws_sdk.py @@ -9,7 +9,8 @@ from __future__ import annotations import argparse -from typing import Protocol +from collections.abc import Mapping +from typing import Protocol, cast import boto3 @@ -17,18 +18,34 @@ from alternator import Auth +class DynamoDBServiceModelLike(Protocol): + """Minimal service model protocol used by this example.""" + + service_name: str + + +class DynamoDBClientMetaLike(Protocol): + """Minimal DynamoDB client metadata protocol used by this example.""" + + service_model: DynamoDBServiceModelLike + endpoint_url: str + + class DynamoDBLikeClient(Protocol): """Minimal DynamoDB client protocol used by this example.""" @property - def meta(self) -> object: ... + def meta(self) -> DynamoDBClientMetaLike: + pass - def list_tables(self) -> dict[str, object]: ... + def list_tables(self) -> Mapping[str, object]: + pass -def print_client_details(name: str, client: DynamoDBLikeClient) -> None: +def print_client_details(name: str, client: object) -> None: """Print comparable client metadata.""" - meta = client.meta + typed_client = cast(DynamoDBLikeClient, client) + meta = typed_client.meta print(f"{name}:") print(f" service: {meta.service_model.service_name}") print(f" endpoint: {meta.endpoint_url}") diff --git a/examples/sync_demo.py b/examples/sync_demo.py index dcf614c..a45de46 100644 --- a/examples/sync_demo.py +++ b/examples/sync_demo.py @@ -11,6 +11,7 @@ import logging import os import sys +from typing import Any, cast from botocore.exceptions import ClientError @@ -37,8 +38,8 @@ def main() -> None: with AlternatorClient(config) as client: # List existing tables print("Listing tables...") - response = client.list_tables() - tables = response.get("TableNames", []) + list_response = client.list_tables() + tables = list_response.get("TableNames", []) print(f"Found {len(tables)} tables: {tables}") # Create a test table if it doesn't exist @@ -75,11 +76,11 @@ def main() -> None: # Read items back print("Reading items...") for i in range(5): - response = client.get_item( + get_response = client.get_item( TableName=table_name, Key={"pk": {"S": f"user_{i}"}}, ) - item = response.get("Item", {}) + item = cast(dict[str, Any], get_response.get("Item", {})) print(f" user_{i}: {item.get('data', {}).get('S', 'N/A')}") print("Demo complete!") diff --git a/pyproject.toml b/pyproject.toml index 56d868b..203522f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,15 +70,20 @@ python_version = "3.10" strict = true warn_return_any = true warn_unused_configs = true +warn_unreachable = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = true +disallow_any_unimported = true no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true follow_imports = "silent" + +[[tool.mypy.overrides]] +module = ["aioboto3"] ignore_missing_imports = true [tool.ruff] diff --git a/tests/unit/test_async_manager.py b/tests/unit/test_async_manager.py index 9178f69..45c47ea 100644 --- a/tests/unit/test_async_manager.py +++ b/tests/unit/test_async_manager.py @@ -1,6 +1,7 @@ """Tests for AsyncLiveNodesManager and AsyncPartitionKeyCache.""" import asyncio +from typing import Any, cast from unittest.mock import AsyncMock, MagicMock import pytest @@ -39,11 +40,11 @@ async def mock_fetch(url: str) -> list[str]: # Start await manager.start() - assert manager._refresh_task is not None + assert cast(object, manager._refresh_task) is not None # Stop await manager.stop() - assert manager._refresh_task is None + assert cast(object, manager._refresh_task) is None @pytest.mark.asyncio async def test_next_node_uri_format(self, config: Config) -> None: @@ -152,7 +153,11 @@ async def mock_fetch(url: str) -> list[str]: @pytest.mark.asyncio async def test_url_construction(self, config: Config) -> None: """Test build_localnodes_url constructs correct URL.""" - manager = AsyncLiveNodesManager(config, lambda _: []) + + async def empty_fetch(url: str) -> list[str]: + return [] + + manager = AsyncLiveNodesManager(config, empty_fetch) url = manager._core.build_localnodes_url(ClusterScope(), "192.168.1.1") assert url == "http://192.168.1.1:8000/localnodes" @@ -160,7 +165,11 @@ async def test_url_construction(self, config: Config) -> None: @pytest.mark.asyncio async def test_url_construction_with_dc_scope(self, config: Config) -> None: """Test URL construction with datacenter scope.""" - manager = AsyncLiveNodesManager(config, lambda _: []) + + async def empty_fetch(url: str) -> list[str]: + return [] + + manager = AsyncLiveNodesManager(config, empty_fetch) url = manager._core.build_localnodes_url( DatacenterScope(datacenter="dc1"), "192.168.1.1" @@ -380,7 +389,7 @@ async def test_concurrent_requests_single_fetch(self) -> None: fetch_count = 0 fetch_event = asyncio.Event() - async def slow_describe_table(TableName: str) -> dict: + async def slow_describe_table(TableName: str) -> dict[str, Any]: nonlocal fetch_count fetch_count += 1 # Wait a bit to simulate network delay diff --git a/tests/unit/test_boto_config.py b/tests/unit/test_boto_config.py index 7924095..769f0b0 100644 --- a/tests/unit/test_boto_config.py +++ b/tests/unit/test_boto_config.py @@ -2,6 +2,7 @@ import contextlib from pathlib import Path +from typing import Any, cast import pytest from botocore import UNSIGNED @@ -26,12 +27,12 @@ def _make_config(self, **overrides: object) -> Config: "port": 9998, } defaults.update(overrides) - return Config(**defaults) # type: ignore[arg-type] -- test helper + return Config(**cast(Any, defaults)) def test_unsigned_when_no_credentials(self) -> None: """Without credentials the signature must be UNSIGNED.""" config = self._make_config() - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) # BotoConfig stores user-provided options in _user_provided_options assert boto_config.signature_version is UNSIGNED @@ -39,7 +40,7 @@ def test_unsigned_when_no_credentials(self) -> None: def test_no_unsigned_when_credentials_provided(self) -> None: """With credentials the signature_version must not be set.""" config = self._make_config() - boto_config = _create_boto_config(config, auth_enabled=True) + boto_config: Any = _create_boto_config(config, auth_enabled=True) assert "signature_version" not in boto_config._user_provided_options @@ -48,7 +49,7 @@ def test_retry_settings_propagated(self) -> None: config = self._make_config( retries=RetryConfig(max_attempts=5, mode=RetryMode.ADAPTIVE), ) - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) retries = boto_config._user_provided_options["retries"] assert retries["total_max_attempts"] == 5 @@ -59,7 +60,7 @@ def test_retry_attempts_are_total_attempts_in_created_client(self) -> None: import boto3 config = self._make_config(retries=RetryConfig(max_attempts=3)) - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) client = boto3.client( "dynamodb", endpoint_url="http://localhost:1", @@ -67,12 +68,12 @@ def test_retry_attempts_are_total_attempts_in_created_client(self) -> None: region_name="us-east-1", ) - assert client.meta.config.retries["total_max_attempts"] == 3 + assert cast(Any, client.meta.config).retries["total_max_attempts"] == 3 def test_pool_connections_propagated(self) -> None: """max_pool_connections from Config flows into BotoConfig.""" config = self._make_config(max_pool_connections=42) - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.max_pool_connections == 42 @@ -85,7 +86,7 @@ def test_timeouts_propagated(self) -> None: read_seconds=7.5, ), ) - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.connect_timeout == 2.5 assert boto_config.read_timeout == 7.5 @@ -93,7 +94,7 @@ def test_timeouts_propagated(self) -> None: def test_default_user_agent_propagated(self) -> None: """By default Alternator supplies its own SDK user-agent.""" config = self._make_config() - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.user_agent == DEFAULT_USER_AGENT assert boto_config._user_provided_options["user_agent"] == DEFAULT_USER_AGENT @@ -103,28 +104,28 @@ def test_user_agent_callback_can_append_to_current_value(self) -> None: current = "Boto3/1.0 Botocore/1.0" config = self._make_config(user_agent=lambda default: f"{current} {default}") - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.user_agent == f"Boto3/1.0 Botocore/1.0 {DEFAULT_USER_AGENT}" def test_user_agent_callback_can_wrap_default_value(self) -> None: """Callback can place the Alternator identity inside another string.""" config = self._make_config(user_agent=lambda default: f"service-a ({default})") - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.user_agent == f"service-a ({DEFAULT_USER_AGENT})" def test_user_agent_callback_can_replace_default_value(self) -> None: """Callback can replace the Alternator identity completely.""" config = self._make_config(user_agent=lambda _default: "orders-service/1.0") - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.user_agent == "orders-service/1.0" def test_user_agent_string_replaces_default_value(self) -> None: """A literal user-agent value replaces the Alternator identity.""" config = self._make_config(user_agent="orders-service/1.0") - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.user_agent == "orders-service/1.0" @@ -151,7 +152,7 @@ def test_client_cert_propagated_for_https(self) -> None: client_key_path=Path("/path/to/client.key"), ), ) - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.client_cert == ( "/path/to/client.crt", @@ -164,7 +165,7 @@ def test_combined_client_cert_propagated_for_https(self) -> None: scheme="https", tls=TLS(client_cert_path=Path("/path/to/client-combined.pem")), ) - boto_config = _create_boto_config(config, auth_enabled=False) + boto_config: Any = _create_boto_config(config, auth_enabled=False) assert boto_config.client_cert == "/path/to/client-combined.pem" @@ -194,7 +195,7 @@ def test_async_config_matches_sync_transport_settings(self) -> None: read_seconds=11.0, ), ) - aio_config = _create_aio_config(config, auth_enabled=False) + aio_config: Any = _create_aio_config(config, auth_enabled=False) retries = aio_config._user_provided_options["retries"] assert retries["total_max_attempts"] == 4 @@ -252,4 +253,4 @@ def _make_config(**overrides: object) -> Config: "port": 9998, } defaults.update(overrides) - return Config(**defaults) # type: ignore[arg-type] -- test helper + return Config(**cast(Any, defaults)) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 9a5f945..d0d736f 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,6 +1,7 @@ """Tests for configuration classes.""" from pathlib import Path +from typing import Any, cast import pytest @@ -56,7 +57,7 @@ def test_invalid_scheme_raises(self) -> None: Config( seed_hosts=["localhost"], port=8000, - scheme="ftp", + scheme=cast(Any, "ftp"), ) def test_invalid_port_zero_raises(self) -> None: @@ -93,6 +94,7 @@ def test_default_values(self) -> None: assert config.node_list_polling.active_interval_ms == 1000 assert config.node_list_polling.idle_interval_ms == 60000 assert config.aws_region == "us-east-1" + assert isinstance(config.user_agent, str) assert config.user_agent.startswith("alternator-client-python/") assert isinstance(config.routing_scope, ClusterScope) @@ -256,7 +258,7 @@ def test_with_response_compression_rejects_invalid_encoding(self) -> None: match="unsupported response compression encoding", ): AlternatorConfigBuilder().with_response_compression( - None, # type: ignore[arg-type] -- validate runtime input + None, # type: ignore[arg-type] # validate runtime input ) def test_invalid_response_compression_raises(self) -> None: @@ -268,7 +270,7 @@ def test_invalid_response_compression_raises(self) -> None: Config( seed_hosts=["localhost"], port=8000, - response_compression=("br",), # type: ignore[arg-type] -- validate runtime input + response_compression=("br",), # type: ignore[arg-type] # validate runtime input ) @pytest.mark.parametrize("gzip_level", [-1, 10]) diff --git a/tests/unit/test_helper.py b/tests/unit/test_helper.py index 01ea447..cbe0830 100644 --- a/tests/unit/test_helper.py +++ b/tests/unit/test_helper.py @@ -188,8 +188,8 @@ def test_close_client_closes_underlying_boto_client() -> None: setattr(client, MANAGER_ATTR, manager) setattr(client, MANAGER_OWNS_ATTR, True) - close_client(client) # type: ignore[arg-type] -- lightweight boto client stub - close_client(client) # type: ignore[arg-type] -- idempotency check + close_client(client) # type: ignore[arg-type] # lightweight boto client stub + close_client(client) # type: ignore[arg-type] # idempotency check assert manager.stop.call_count == 1 assert client.close.call_count == 2 @@ -203,7 +203,7 @@ def test_close_resource_closes_underlying_boto_client() -> None: setattr(resource, MANAGER_ATTR, manager) setattr(resource, MANAGER_OWNS_ATTR, True) - close_client(resource) # type: ignore[arg-type] -- lightweight resource stub + close_client(resource) # type: ignore[arg-type] # lightweight resource stub manager.stop.assert_called_once_with() service_client.close.assert_called_once_with() diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 95478e6..7e1464d 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -50,7 +50,7 @@ def test_fetches_json_node_list(self, mock_server: HTTPServer) -> None: MockHTTPHandler.response_code = 200 fetcher = create_sync_http_fetcher() - host, port = mock_server.server_address + host, port = "127.0.0.1", mock_server.server_port url = f"http://{host}:{port}/localnodes" nodes = fetcher(url) @@ -63,7 +63,7 @@ def test_returns_empty_on_empty_response(self, mock_server: HTTPServer) -> None: MockHTTPHandler.response_code = 200 fetcher = create_sync_http_fetcher() - host, port = mock_server.server_address + host, port = "127.0.0.1", mock_server.server_port url = f"http://{host}:{port}/localnodes" nodes = fetcher(url) @@ -99,7 +99,7 @@ def log_message(self, format: str, *args: object) -> None: thread.start() fetcher = create_sync_http_fetcher() - host, port = server.server_address + host, port = "127.0.0.1", server.server_port url = f"http://{host}:{port}/localnodes" nodes = fetcher(url) @@ -113,7 +113,7 @@ def test_returns_empty_on_http_404(self, mock_server: HTTPServer) -> None: MockHTTPHandler.response_code = 404 fetcher = create_sync_http_fetcher() - host, port = mock_server.server_address + host, port = "127.0.0.1", mock_server.server_port url = f"http://{host}:{port}/localnodes" nodes = fetcher(url) @@ -126,7 +126,7 @@ def test_returns_empty_on_http_500(self, mock_server: HTTPServer) -> None: MockHTTPHandler.response_code = 500 fetcher = create_sync_http_fetcher() - host, port = mock_server.server_address + host, port = "127.0.0.1", mock_server.server_port url = f"http://{host}:{port}/localnodes" nodes = fetcher(url) @@ -139,7 +139,7 @@ def test_returns_empty_on_http_503(self, mock_server: HTTPServer) -> None: MockHTTPHandler.response_code = 503 fetcher = create_sync_http_fetcher() - host, port = mock_server.server_address + host, port = "127.0.0.1", mock_server.server_port url = f"http://{host}:{port}/localnodes" nodes = fetcher(url) diff --git a/tests/unit/test_key_affinity.py b/tests/unit/test_key_affinity.py index 1ca93f2..fff7aa9 100644 --- a/tests/unit/test_key_affinity.py +++ b/tests/unit/test_key_affinity.py @@ -26,9 +26,11 @@ from alternator.core.live_nodes import NodeList from alternator.core.request import extract_request_params +Params = dict[str, Any] + def _batch_write_routing_target( - params: dict[str, Any], + params: Params, ) -> tuple[str | None, tuple[str, Any] | None, int | None]: table_name = get_table_name(params) pk = extract_partition_key(params, "pk") @@ -69,7 +71,7 @@ def test_put_with_empty_condition_is_not_rmw(self) -> None: def test_put_with_expected_is_rmw(self) -> None: """Test PutItem with Expected is RMW.""" - params = {"Expected": {}} + params: Params = {"Expected": {}} assert is_rmw_operation("PutItem", params) is True def test_put_with_all_old_return_values_is_rmw(self) -> None: @@ -94,7 +96,7 @@ def test_delete_with_empty_condition_is_not_rmw(self) -> None: def test_delete_with_expected_is_rmw(self) -> None: """Test DeleteItem with Expected is RMW.""" - params = {"Expected": {}} + params: Params = {"Expected": {}} assert is_rmw_operation("DeleteItem", params) is True def test_delete_with_all_old_return_values_is_rmw(self) -> None: @@ -134,7 +136,7 @@ def test_update_with_empty_update_expression_is_not_rmw(self) -> None: def test_update_with_expected_is_rmw(self) -> None: """Test UpdateItem with Expected is RMW.""" - params = {"Expected": {}} + params: Params = {"Expected": {}} assert is_rmw_operation("UpdateItem", params) is True def test_update_with_empty_return_values_is_not_rmw(self) -> None: @@ -178,7 +180,7 @@ def test_get_item_is_not_rmw(self) -> None: def test_scan_is_not_rmw(self) -> None: """Test Scan is never RMW.""" - params = {} + params: Params = {} assert is_rmw_operation("Scan", params) is False @@ -230,7 +232,7 @@ def test_rmw_mode_with_rmw_operation(self) -> None: def test_rmw_mode_with_non_rmw_operation(self) -> None: """Test RMW mode with non-RMW operation.""" - params = {} + params: Params = {} assert should_use_affinity("RMW", "PutItem", params) is False def test_rmw_mode_with_batch_write(self) -> None: @@ -252,7 +254,7 @@ def test_rmw_mode_with_batch_write(self) -> None: def test_any_write_mode_with_write(self) -> None: """Test ANY_WRITE mode with write operation.""" - params = {} + params: Params = {} assert should_use_affinity("ANY_WRITE", "PutItem", params) is True assert should_use_affinity("ANY_WRITE", "UpdateItem", params) is True assert should_use_affinity("ANY_WRITE", "DeleteItem", params) is True @@ -260,7 +262,7 @@ def test_any_write_mode_with_write(self) -> None: def test_any_write_mode_with_read(self) -> None: """Test ANY_WRITE mode with read operation.""" - params = {} + params: Params = {} assert should_use_affinity("ANY_WRITE", "GetItem", params) is False assert should_use_affinity("ANY_WRITE", "Query", params) is False @@ -464,13 +466,16 @@ def test_batch_write_missing_pk_metadata_falls_back(self) -> None: } } + def no_pk_name(table_name: str) -> str | None: + return None + assert ( select_affinity_node( mode="ANY_WRITE", operation_name="BatchWriteItem", params=params, nodes=nodes, - get_pk_name={}.get, + get_pk_name=no_pk_name, ) is None ) diff --git a/tests/unit/test_live_nodes.py b/tests/unit/test_live_nodes.py index c9e74e9..15318f7 100644 --- a/tests/unit/test_live_nodes.py +++ b/tests/unit/test_live_nodes.py @@ -38,7 +38,7 @@ def test_nodelist_is_immutable(self) -> None: """Test that NodeList is frozen.""" nodes = NodeList(nodes=("host1",), scope_name="Cluster") with pytest.raises(AttributeError): - nodes.nodes = ("host2",) # type: ignore[misc] -- testing frozen dataclass immutability + nodes.nodes = ("host2",) # type: ignore[misc] # testing frozen dataclass immutability class TestRoundRobinSelector: diff --git a/tests/unit/test_network_failures.py b/tests/unit/test_network_failures.py index 1c6ea1b..1260115 100644 --- a/tests/unit/test_network_failures.py +++ b/tests/unit/test_network_failures.py @@ -63,7 +63,7 @@ def log_message(self, format: str, *args: object) -> None: def _get_url(server: HTTPServer, path: str = "/localnodes") -> str: - host, port = server.server_address + host, port = "127.0.0.1", server.server_port return f"http://{host}:{port}{path}" @@ -232,7 +232,9 @@ def unused_port(self) -> int: """Get a port that is guaranteed to be unused.""" with socket.socket() as s: s.bind(("127.0.0.1", 0)) - return s.getsockname()[1] + port = s.getsockname()[1] + assert isinstance(port, int) + return port @pytest.mark.asyncio async def test_connection_refused(self, unused_port: int) -> None: diff --git a/tests/unit/test_request_lifecycle.py b/tests/unit/test_request_lifecycle.py index 0521bff..5ebbc03 100644 --- a/tests/unit/test_request_lifecycle.py +++ b/tests/unit/test_request_lifecycle.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast import boto3 import pytest @@ -219,9 +219,9 @@ def retry_without_sleep(attempts: int, **_: object) -> int | None: ) client.meta.events.register_first( "needs-retry.dynamodb.PutItem", - retry_without_sleep, + cast(Any, retry_without_sleep), ) - monkeypatch.setattr(client._endpoint.http_session, "send", fail_send) + monkeypatch.setattr(cast(Any, client)._endpoint.http_session, "send", fail_send) with pytest.raises(EndpointConnectionError): client.put_item(TableName="tbl", Item={"pk": {"S": "k"}}) diff --git a/tests/unit/test_tls.py b/tests/unit/test_tls.py index f414ba2..8733f96 100644 --- a/tests/unit/test_tls.py +++ b/tests/unit/test_tls.py @@ -264,4 +264,4 @@ def test_immutable(self) -> None: config = TlsSessionCacheConfig() with pytest.raises(AttributeError): - config.enabled = False # type: ignore[misc] -- testing frozen dataclass immutability + config.enabled = False # type: ignore[misc] # testing frozen dataclass immutability