[PATCH 10/14] Add authheaders stub and typed callable

Tamir Duberstein <[email protected]>
Newsgroups org.kernel.linux.tools
Message-ID <[email protected]>
Add a generated authheaders stub under typings so the type checkers can
resolve the package. Then store authenticate_message directly on
LoreNode as a typed callable and update the authheaders tests to use
that shape.

Signed-off-by: Tamir Duberstein <[email protected]>
---
 src/liblore/node.py              | 26 +++++++++++++++++++-------
 tests/test_auth_headers.py       | 31 ++++++++++++++++++-------------
 typings/authheaders/__init__.pyi | 13 +++++++++++++
 3 files changed, 50 insertions(+), 20 deletions(-)

diff --git a/src/liblore/node.py b/src/liblore/node.py
index 4835169..4d38bdf 100644
--- a/src/liblore/node.py
+++ b/src/liblore/node.py
@@ -13,11 +13,10 @@ import os
 import re
 import subprocess
 import time
-import types
 import urllib.parse
 from datetime import datetime, timezone
 from email.message import EmailMessage
-from typing import TYPE_CHECKING, TypedDict
+from typing import TYPE_CHECKING, Protocol, TypedDict
 
 import requests
 
@@ -44,6 +43,19 @@ class _LoreNodeInitKwargs(TypedDict, total=False):
     cache_ttl: int
 
 
+class _AuthenticateMessage(Protocol):
+    def __call__(
+        self,
+        msg: bytes,
+        authserv_id: str,
+        *,
+        dkim: bool = ...,
+        dmarc: bool = ...,
+        arc: bool = ...,
+        spf: bool = ...,
+    ) -> str: ...
+
+
 def _get_config_from_git(
     regexp: str,
     multivals: list[str] | None = None,
@@ -206,12 +218,12 @@ class LoreNode:
 
         if cache_dir is not None:
             os.makedirs(cache_dir, exist_ok=True)
-        self._authheaders: types.ModuleType | None = None
+        self._authenticate_message: _AuthenticateMessage | None = None
         if add_auth_headers:
             try:
-                import authheaders  # type: ignore[import-untyped]
+                import authheaders
 
-                self._authheaders = authheaders
+                self._authenticate_message = authheaders.authenticate_message
             except ImportError:
                 raise LibloreError(
                     'authheaders library is required for add_auth_headers. '
@@ -721,11 +733,11 @@ class LoreNode:
 
     def _authenticate_msgs(self, msgs: list[EmailMessage]) -> None:
         """Add Authentication-Results headers via authheaders."""
-        if self._authheaders is None:
+        if self._authenticate_message is None:
             return
         for msg in msgs:
             msg_bytes = msg.as_bytes()
-            auth_result = self._authheaders.authenticate_message(
+            auth_result = self._authenticate_message(
                 msg_bytes,
                 'liblore',
                 dkim=True,
diff --git a/tests/test_auth_headers.py b/tests/test_auth_headers.py
index b51050c..e149863 100644
--- a/tests/test_auth_headers.py
+++ b/tests/test_auth_headers.py
@@ -14,7 +14,12 @@ import pytest
 import responses
 
 from liblore import LibloreError
-from liblore.node import LoreNode
+from liblore.node import LoreNode, _AuthenticateMessage
+
+
+class _FakeAuthHeaders(ModuleType):
+    authenticate_message: _AuthenticateMessage
+
 
 # =====================================================================
 # Import-time validation
@@ -28,16 +33,16 @@ class TestAuthHeadersImport:
                 LoreNode(add_auth_headers=True)
 
     def test_ok_when_authheaders_installed(self) -> None:
-        fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock()  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
+        fake = _FakeAuthHeaders('authheaders')
+        fake.authenticate_message = MagicMock()
         with patch.dict(sys.modules, {'authheaders': fake}):
             node = LoreNode(add_auth_headers=True)
-            assert node._authheaders is not None
+            assert node._authenticate_message is not None
             node.close()
 
     def test_default_is_disabled(self) -> None:
         node = LoreNode()
-        assert node._authheaders is None
+        assert node._authenticate_message is None
         node.close()
 
 
@@ -55,8 +60,8 @@ class TestAuthenticateMsgs:
         assert 'Authentication-Results' not in msg
 
     def test_adds_header_when_enabled(self) -> None:
-        fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
+        fake = _FakeAuthHeaders('authheaders')
+        fake.authenticate_message = MagicMock(
             return_value='Authentication-Results: liblore; dkim=pass header.d=example.com',
         )
         with patch.dict(sys.modules, {'authheaders': fake}):
@@ -81,8 +86,8 @@ class TestAuthenticateMsgs:
             node.close()
 
     def test_skips_empty_result(self) -> None:
-        fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(return_value='')  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
+        fake = _FakeAuthHeaders('authheaders')
+        fake.authenticate_message = MagicMock(return_value='')
         with patch.dict(sys.modules, {'authheaders': fake}):
             node = LoreNode(add_auth_headers=True)
             msg = EmailMessage()
@@ -94,8 +99,8 @@ class TestAuthenticateMsgs:
             node.close()
 
     def test_multiple_messages(self) -> None:
-        fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
+        fake = _FakeAuthHeaders('authheaders')
+        fake.authenticate_message = MagicMock(
             side_effect=[
                 'liblore; dkim=pass',
                 'Authentication-Results: liblore; dkim=fail',
@@ -125,8 +130,8 @@ class TestAuthenticateMsgs:
 class TestAuthInFetchMethods:
     @pytest.fixture()
     def auth_node(self) -> Iterator[tuple[LoreNode, responses.RequestsMock]]:
-        fake = ModuleType('authheaders')
-        fake.authenticate_message = MagicMock(  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
+        fake = _FakeAuthHeaders('authheaders')
+        fake.authenticate_message = MagicMock(
             return_value='Authentication-Results: liblore; dkim=pass',
         )
         with patch.dict(sys.modules, {'authheaders': fake}):
diff --git a/typings/authheaders/__init__.pyi b/typings/authheaders/__init__.pyi
new file mode 100644
index 0000000..ed4f61e
--- /dev/null
+++ b/typings/authheaders/__init__.pyi
@@ -0,0 +1,13 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+def authenticate_message(
+    msg: bytes,
+    authserv_id: str,
+    *,
+    dkim: bool = ...,
+    dmarc: bool = ...,
+    arc: bool = ...,
+    spf: bool = ...,
+) -> str: ...

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