diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d345f02..f32553c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,6 +33,14 @@ jobs: - name: Type check run: uv run ty check + - name: Fail on type: ignore (no suppressions allowed) + run: | + if grep -rE '# type: ignore|# ty: ignore' --include='*.py' . --exclude-dir=.venv --exclude-dir=.git; then + echo "::error::type: ignore / ty: ignore comments are not allowed. Fix the underlying type errors instead." + exit 1 + fi + exit 0 + - name: Format run: uv run ruff format diff --git a/api/request_utils.py b/api/request_utils.py index 4b3a04c..28a3790 100644 --- a/api/request_utils.py +++ b/api/request_utils.py @@ -5,7 +5,7 @@ Contains token counting for API requests. import json import logging -from typing import Any, List, Optional, Union +from typing import Any, List, Optional, Union, cast import tiktoken @@ -18,7 +18,7 @@ __all__ = ["get_token_count"] def _get_block_attr(block: object, key: str, default: Any = "") -> Any: """Get attribute from block (object or dict).""" if isinstance(block, dict): - return block.get(key, default) # type: ignore[no-matching-overload] + return cast(dict[str, Any], block).get(key, default) return getattr(block, key, default) diff --git a/messaging/tree_data.py b/messaging/tree_data.py index 7d13659..e38f44c 100644 --- a/messaging/tree_data.py +++ b/messaging/tree_data.py @@ -9,7 +9,7 @@ from collections import deque from contextlib import asynccontextmanager from enum import Enum from datetime import datetime, timezone -from typing import Dict, Optional, List, Any +from typing import Dict, Optional, List, Any, cast from dataclasses import dataclass, field from .models import IncomingMessage @@ -265,7 +265,8 @@ class MessageTree: async with self._lock: # Read internal deque directly to avoid mutating queue state. # Drain/put approach would inflate _unfinished_tasks without task_done(). - return list(self._queue._queue) # type: ignore[attr-defined] + queue_deque = cast(deque, getattr(self._queue, "_queue")) + return list(queue_deque) def get_queue_size(self) -> int: """Get number of messages waiting in queue.""" @@ -281,10 +282,12 @@ class MessageTree: Note: asyncio.Queue has no built-in remove; we filter via the internal deque. O(n) in queue size; acceptable for typical tree queue sizes. """ - queue_deque: deque = self._queue._queue # type: ignore[attr-defined] + queue_deque = cast(deque, getattr(self._queue, "_queue")) if node_id not in queue_deque: return False - self._queue._queue = deque(x for x in queue_deque if x != node_id) # type: ignore[attr-defined] + object.__setattr__( + self._queue, "_queue", deque(x for x in queue_deque if x != node_id) + ) return True @asynccontextmanager diff --git a/providers/rate_limit.py b/providers/rate_limit.py index 88413e4..b343ee8 100644 --- a/providers/rate_limit.py +++ b/providers/rate_limit.py @@ -193,4 +193,5 @@ class GlobalRateLimiter: self.set_blocked(delay) await asyncio.sleep(delay) - raise last_exc # type: ignore[misc] + assert last_exc is not None + raise last_exc diff --git a/tests/conftest.py b/tests/conftest.py index 206ba2a..cd78de1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ os.environ["PTB_TIMEDELTA"] = "1" # Add project root to path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from typing import Any from unittest.mock import AsyncMock, MagicMock from providers.base import ProviderConfig from providers.nvidia_nim import NvidiaNimProvider @@ -91,8 +92,22 @@ def mock_session_store(): @pytest.fixture def incoming_message_factory(): + _valid_keys = frozenset( + { + "text", + "chat_id", + "user_id", + "message_id", + "platform", + "reply_to_message_id", + "username", + "timestamp", + "raw_event", + } + ) + def _create(**kwargs): - defaults = { + defaults: dict[str, Any] = { "text": "hello", "chat_id": "chat_1", "user_id": "user_1", @@ -104,6 +119,7 @@ def incoming_message_factory(): from datetime import datetime defaults["timestamp"] = datetime.fromisoformat(defaults["timestamp"]) - return IncomingMessage(**defaults) # type: ignore + filtered = {k: v for k, v in defaults.items() if k in _valid_keys} + return IncomingMessage(**filtered) return _create diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 7c2fc3c..da6b56e 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from messaging.transcript import TranscriptBuffer, RenderCtx from messaging.telegram_markdown import ( escape_md_v2, @@ -163,9 +165,8 @@ def test_transcript_render_segment_exception_skipped(): def _raising_render(self, ctx): raise ValueError("render failed") - bad_segment.render = _raising_render # type: ignore[method-assign] - - out = t.render(_ctx(), limit_chars=3900, status=None) + with patch.object(bad_segment, "render", _raising_render): + out = t.render(_ctx(), limit_chars=3900, status=None) assert "before" in out assert "after" in out assert "middle" not in out