[PATCH v2 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 f9817c1..8935dbf 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