[PATCH 07/14] Add ty checks to CI

Tamir Duberstein <[email protected]>
Newsgroups org.kernel.linux.tools
Message-ID <[email protected]>
Add ty to the development dependencies and enable all ty rules. Run ty
in the b4 CI check script so its diagnostics are surfaced with the other
checks.

Use `ty check --add-ignore` to suppress existing errors.

Signed-off-by: Tamir Duberstein <[email protected]>
---
 pyproject.toml             |  4 ++++
 src/liblore/node.py        |  2 +-
 tests/test_auth_headers.py | 10 +++++-----
 tests/test_node.py         | 30 +++++++++++++++---------------
 tools/b4-ci-check.py       |  6 ++++++
 5 files changed, 31 insertions(+), 21 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index a160edd..37c85a7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,6 +44,7 @@ dev = [
     "pytest-asyncio",
     "responses",
     "ruff",
+    "ty",
     "types-requests",
 ]
 
@@ -63,6 +64,9 @@ executionEnvironments = [
     { root = "tests", reportPrivateUsage = false },
 ]
 
+[tool.ty.rules]
+all = "error"
+
 [tool.ruff.lint]
 extend-select = ["ARG", "I"]
 
diff --git a/src/liblore/node.py b/src/liblore/node.py
index 095ba4f..1b0463e 100644
--- a/src/liblore/node.py
+++ b/src/liblore/node.py
@@ -319,7 +319,7 @@ class LoreNode:
                 except ValueError:
                     pass
 
-        node = cls(url, **kwargs)  # type: ignore[arg-type]
+        node = cls(url, **kwargs)  # type: ignore[arg-type]  # ty:ignore[invalid-argument-type]
 
         val = gitcfg.get('useragentplus')
         if isinstance(val, str) and val:
diff --git a/tests/test_auth_headers.py b/tests/test_auth_headers.py
index f1c240a..b51050c 100644
--- a/tests/test_auth_headers.py
+++ b/tests/test_auth_headers.py
@@ -29,7 +29,7 @@ class TestAuthHeadersImport:
 
     def test_ok_when_authheaders_installed(self) -> None:
         fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock()  # type: ignore[attr-defined]
+        fake.authenticate_message = MagicMock()  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
         with patch.dict(sys.modules, {'authheaders': fake}):
             node = LoreNode(add_auth_headers=True)
             assert node._authheaders is not None
@@ -56,7 +56,7 @@ class TestAuthenticateMsgs:
 
     def test_adds_header_when_enabled(self) -> None:
         fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]
+        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
             return_value='Authentication-Results: liblore; dkim=pass header.d=example.com',
         )
         with patch.dict(sys.modules, {'authheaders': fake}):
@@ -82,7 +82,7 @@ class TestAuthenticateMsgs:
 
     def test_skips_empty_result(self) -> None:
         fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(return_value='')  # type: ignore[attr-defined]
+        fake.authenticate_message = MagicMock(return_value='')  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
         with patch.dict(sys.modules, {'authheaders': fake}):
             node = LoreNode(add_auth_headers=True)
             msg = EmailMessage()
@@ -95,7 +95,7 @@ class TestAuthenticateMsgs:
 
     def test_multiple_messages(self) -> None:
         fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]
+        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
             side_effect=[
                 'liblore; dkim=pass',
                 'Authentication-Results: liblore; dkim=fail',
@@ -126,7 +126,7 @@ class TestAuthInFetchMethods:
     @pytest.fixture()
     def auth_node(self) -> Iterator[tuple[LoreNode, responses.RequestsMock]]:
         fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]
+        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
             return_value='Authentication-Results: liblore; dkim=pass',
         )
         with patch.dict(sys.modules, {'authheaders': fake}):
diff --git a/tests/test_node.py b/tests/test_node.py
index fc94d9f..f4a3495 100644
--- a/tests/test_node.py
+++ b/tests/test_node.py
@@ -500,19 +500,19 @@ class TestBatchGetThreadByMsgid:
         node = LoreNode()
         thread_a = [EmailMessage()]
         thread_b = [EmailMessage(), EmailMessage()]
-        node.get_thread_by_msgid = MagicMock(side_effect=[thread_a, thread_b])  # type: ignore[method-assign]
+        node.get_thread_by_msgid = MagicMock(side_effect=[thread_a, thread_b])  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             results = node.batch_get_thread_by_msgid(['a@x', 'b@x'])
 
         assert results == [thread_a, thread_b]
-        assert node.get_thread_by_msgid.call_count == 2
+        assert node.get_thread_by_msgid.call_count == 2  # ty:ignore[unresolved-attribute]
         mock_sleep.assert_called_once_with(0.1)
 
     def test_no_sleep_for_single_msgid(self) -> None:
         node = LoreNode()
         thread = [EmailMessage()]
-        node.get_thread_by_msgid = MagicMock(return_value=thread)  # type: ignore[method-assign]
+        node.get_thread_by_msgid = MagicMock(return_value=thread)  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             results = node.batch_get_thread_by_msgid(['only@x'])
@@ -522,7 +522,7 @@ class TestBatchGetThreadByMsgid:
 
     def test_passes_kwargs(self) -> None:
         node = LoreNode()
-        node.get_thread_by_msgid = MagicMock(return_value=[EmailMessage()])  # type: ignore[method-assign]
+        node.get_thread_by_msgid = MagicMock(return_value=[EmailMessage()])  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep'):
             node.batch_get_thread_by_msgid(
@@ -532,7 +532,7 @@ class TestBatchGetThreadByMsgid:
                 since='20240101',
             )
 
-        node.get_thread_by_msgid.assert_called_once_with(
+        node.get_thread_by_msgid.assert_called_once_with(  # ty:ignore[unresolved-attribute]
             'a@x',
             strict=False,
             sort=True,
@@ -541,7 +541,7 @@ class TestBatchGetThreadByMsgid:
 
     def test_sleep_count_matches_gaps(self) -> None:
         node = LoreNode()
-        node.get_thread_by_msgid = MagicMock(return_value=[EmailMessage()])  # type: ignore[method-assign]
+        node.get_thread_by_msgid = MagicMock(return_value=[EmailMessage()])  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             node.batch_get_thread_by_msgid(['a@x', 'b@x', 'c@x'])
@@ -550,14 +550,14 @@ class TestBatchGetThreadByMsgid:
 
     def test_empty_list(self) -> None:
         node = LoreNode()
-        node.get_thread_by_msgid = MagicMock()  # type: ignore[method-assign]
+        node.get_thread_by_msgid = MagicMock()  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             results = node.batch_get_thread_by_msgid([])
 
         assert results == []
         mock_sleep.assert_not_called()
-        node.get_thread_by_msgid.assert_not_called()
+        node.get_thread_by_msgid.assert_not_called()  # ty:ignore[unresolved-attribute]
 
 
 # =====================================================================
@@ -570,19 +570,19 @@ class TestBatchGetThreadByQuery:
         node = LoreNode()
         result_a = [EmailMessage()]
         result_b = [EmailMessage(), EmailMessage()]
-        node.get_thread_by_query = MagicMock(side_effect=[result_a, result_b])  # type: ignore[method-assign]
+        node.get_thread_by_query = MagicMock(side_effect=[result_a, result_b])  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             results = node.batch_get_thread_by_query(['q1', 'q2'])
 
         assert results == [result_a, result_b]
-        assert node.get_thread_by_query.call_count == 2
+        assert node.get_thread_by_query.call_count == 2  # ty:ignore[unresolved-attribute]
         mock_sleep.assert_called_once_with(0.1)
 
     def test_no_sleep_for_single_query(self) -> None:
         node = LoreNode()
         result = [EmailMessage()]
-        node.get_thread_by_query = MagicMock(return_value=result)  # type: ignore[method-assign]
+        node.get_thread_by_query = MagicMock(return_value=result)  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             results = node.batch_get_thread_by_query(['only_query'])
@@ -592,7 +592,7 @@ class TestBatchGetThreadByQuery:
 
     def test_sleep_count_matches_gaps(self) -> None:
         node = LoreNode()
-        node.get_thread_by_query = MagicMock(return_value=[EmailMessage()])  # type: ignore[method-assign]
+        node.get_thread_by_query = MagicMock(return_value=[EmailMessage()])  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             node.batch_get_thread_by_query(['q1', 'q2', 'q3', 'q4'])
@@ -601,14 +601,14 @@ class TestBatchGetThreadByQuery:
 
     def test_empty_list(self) -> None:
         node = LoreNode()
-        node.get_thread_by_query = MagicMock()  # type: ignore[method-assign]
+        node.get_thread_by_query = MagicMock()  # type: ignore[method-assign]  # ty:ignore[invalid-assignment]
 
         with patch('liblore.node.time.sleep') as mock_sleep:
             results = node.batch_get_thread_by_query([])
 
         assert results == []
         mock_sleep.assert_not_called()
-        node.get_thread_by_query.assert_not_called()
+        node.get_thread_by_query.assert_not_called()  # ty:ignore[unresolved-attribute]
 
 
 # =====================================================================
@@ -1928,7 +1928,7 @@ class TestUserAgentPlusProperty:
         """Property has no setter — assignment raises AttributeError."""
         node = LoreNode()
         with pytest.raises(AttributeError):
-            node.user_agent_plus = 'nope'  # type: ignore[misc]
+            node.user_agent_plus = 'nope'  # type: ignore[misc]  # ty:ignore[invalid-assignment]
 
 
 # =====================================================================
diff --git a/tools/b4-ci-check.py b/tools/b4-ci-check.py
index 5690cd9..67278f1 100644
--- a/tools/b4-ci-check.py
+++ b/tools/b4-ci-check.py
@@ -65,6 +65,12 @@ def main() -> None:
             # https://github.com/astral-sh/ruff/issues/659
             run=run_subprocess('ruff'),
         ),
+        Check(
+            tool='ty',
+            args=['check', 'src', 'tests'],
+            pass_summary='ty passed',
+            run=run_subprocess('ty'),
+        ),
         # Mypy can emit JSON via "--output json", but b4 only renders details
         # as plain text, so preserving the normal formatter is more readable.
         Check(

-- 
2.53.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.