ci: enhance type checking in workflow and improve test coverage

- Added a step to fail the CI if any '# type: ignore' comments are found in Python files.
- Refactored tests to use mocking for better isolation and reliability.
- Updated type hints and casting in several files to improve type safety.
This commit is contained in:
Alishahryar1 2026-02-14 23:01:11 -08:00
parent 97217debfa
commit 0d292cd578
6 changed files with 41 additions and 12 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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