From f1e52ff1ace5702025035f0ef3beee22811bff6e Mon Sep 17 00:00:00 2001 From: disqualifier Date: Fri, 3 Jul 2026 00:12:22 -0400 Subject: [PATCH] docs: compress prose/module docstrings, em-dash->hyphen (de-bloat wave 1) Signed-off-by: disqualifier --- README.md | 10 ++++----- pyproject.toml | 2 +- src/aiomail/__init__.py | 9 ++------- src/aiomail/auth.py | 15 +++++--------- src/aiomail/client.py | 45 ++++++++++++++--------------------------- src/aiomail/extract.py | 12 +++-------- src/aiomail/oauth.py | 13 +++++------- src/aiomail/retrieve.py | 13 ++++-------- 8 files changed, 40 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index c4ea736..dee2b85 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,22 @@ This reads codes from email; it does not generate them (that is `pyotp`'s job). `requirements.txt`: ``` -aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.8 +aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.9 # OAuth token providers (Microsoft / Google) need the extra: -aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.8 +aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.9 ``` Direct: ```bash -pip install "aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.8" -pip install "aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.8" +pip install "aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.9" +pip install "aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.9" ``` Requires `aioimaplib` and `beautifulsoup4` (pulled transitively). The `oauth` extra adds `aiohttp` for the refresh-token providers. -Drop the `@v0.1.8` suffix from the line above to install the latest unpinned. +Drop the `@v0.1.9` suffix from the line above to install the latest unpinned. ## Password auth diff --git a/pyproject.toml b/pyproject.toml index a412d49..6b20099 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "aiomail" -version = "0.1.8" +version = "0.1.9" description = "async IMAP one-time-code retrieval with password/OAuth2 auth and dynamic matching" requires-python = ">=3.10" dependencies = [ diff --git a/src/aiomail/__init__.py b/src/aiomail/__init__.py index 1368e76..390b6ed 100644 --- a/src/aiomail/__init__.py +++ b/src/aiomail/__init__.py @@ -1,9 +1,4 @@ -"""aiomail — async IMAP one-time-code retrieval. - -reads OTP / login codes out of IMAP mailboxes (accounts you own). supports plain -password and OAuth2 (XOAUTH2) auth, and dynamic sender/subject/code matching via -substrings, regexes, or callables. -""" +"""aiomail - async IMAP one-time-code retrieval, password or OAuth2 auth, dynamic matching. see README.""" from .auth import Auth, OAuth2Auth, PasswordAuth from .client import IMAPClient from .extract import ( @@ -31,4 +26,4 @@ __all__ = [ "DEFAULT_FOLDERS", ] -__version__ = "0.1.8" +__version__ = "0.1.9" diff --git a/src/aiomail/auth.py b/src/aiomail/auth.py index dc23857..a3937dd 100644 --- a/src/aiomail/auth.py +++ b/src/aiomail/auth.py @@ -1,15 +1,11 @@ -"""authentication mechanisms for the IMAP client. - -`PasswordAuth` (LOGIN) and `OAuth2Auth` (XOAUTH2); credentials always injected. -""" +"""authentication mechanisms for the IMAP client: `PasswordAuth` (LOGIN), `OAuth2Auth` (XOAUTH2).""" import base64 import logging from typing import Awaitable, Callable, Optional, Protocol, Union, runtime_checkable log = logging.getLogger(__name__) -# a token provider is any (optionally async) callable returning a fresh access -# token string; see aiomail.oauth for ready-made Microsoft / Google providers +# see aiomail.oauth for ready-made Microsoft / Google providers TokenProvider = Callable[[], Union[str, Awaitable[str]]] @@ -36,7 +32,7 @@ class PasswordAuth: def _as_str(token) -> str: - """coerce a token to str (a provider may hand back bytes); both XOAUTH2 entrypoints downstream need str""" + """coerce a token to str (a provider may hand back bytes)""" return token.decode() if isinstance(token, bytes) else token @@ -73,9 +69,8 @@ class OAuth2Auth: async def authenticate(self, mail) -> None: token = await self._resolve_token() - # mail.xoauth2(user, token) f-string-interpolates token, so it MUST be str - # — bytes would interpolate the b'...' repr and corrupt the Bearer value. - # _resolve_token already guarantees str via _as_str. + # mail.xoauth2(user, token) f-string-interpolates token, so it MUST be str - + # bytes would interpolate the b'...' repr and corrupt the Bearer value. xoauth2 = getattr(mail, "xoauth2", None) if xoauth2 is not None: result, data = await xoauth2(self.user, token) diff --git a/src/aiomail/client.py b/src/aiomail/client.py index 03cd206..20c0e71 100644 --- a/src/aiomail/client.py +++ b/src/aiomail/client.py @@ -1,9 +1,9 @@ """async IMAP client wrapping aioimaplib: connect/retry/reconnect/close plus folders/search/fetch/mark-seen. auth is injected. reconnect-on-stale re-selects the prior folder, but sequence-number ids from -before a reconnect are not valid after (a fresh SELECT can renumber the mailbox) -— pass `use_uid=True` if ids need to survive a reconnect. one instance is not -safe for concurrent callers beyond the internal connect/reconnect lock. +before a reconnect are not valid after (a fresh SELECT can renumber the mailbox) - pass +`use_uid=True` if ids need to survive a reconnect. one instance is not safe for concurrent +callers beyond the internal connect/reconnect lock. """ import asyncio import email @@ -18,7 +18,7 @@ from .auth import Auth log = logging.getLogger(__name__) -# IMAP LIST reply: (flags) "" — delim is server-defined (often "/" or +# IMAP LIST reply: (flags) "" - delim is server-defined (often "/" or # "." or NIL); capture the trailing name regardless, quoted or bare _LIST_RE = re.compile(rb'^\([^)]*\)\s+(?:"[^"]*"|NIL)\s+(.+)$') @@ -26,9 +26,9 @@ _LIST_RE = re.compile(rb'^\([^)]*\)\s+(?:"[^"]*"|NIL)\s+(.+)$') def _folder_name(raw: bytes) -> Optional[str]: """extract the folder name from a LIST reply line, delimiter-agnostic - returns None on a non-matching line instead of a last-token rsplit fallback, - so the tagged completion line aioimaplib appends to the same response list - (e.g. `b"LIST completed."`) gets dropped instead of read as a phantom folder. + returns None (not a last-token rsplit fallback) on a non-matching line, so the + tagged completion line aioimaplib appends (e.g. `b"LIST completed."`) is dropped + instead of read as a phantom folder. """ match = _LIST_RE.match(raw.strip()) if not match: @@ -39,8 +39,7 @@ def _folder_name(raw: bytes) -> Optional[str]: class IMAPClient: """connection-managing IMAP client driven by an injected auth mechanism - `use_uid` (UID vs sequence-number addressing) is independent of `use_ssl` — - an earlier draft conflated them (`use_uid = use_ssl`); unrelated concerns. + `use_uid` (UID vs sequence-number addressing) is independent of `use_ssl` - unrelated concerns. """ def __init__( @@ -73,11 +72,7 @@ class IMAPClient: await self.close() async def connect(self) -> bool: - """open a connection and authenticate, retrying with linear backoff - - serialized by an internal lock: queued callers never tear down each - other's in-progress handshake, and a superseded connection is logged out. - """ + """open a connection and authenticate, retrying with linear backoff; serialized by an internal lock""" async with self._lock: return await self._connect_locked() @@ -107,19 +102,14 @@ class IMAPClient: @staticmethod async def _discard_mail(mail) -> None: - """tear down a half-built IMAP4 without leaking its connect task - - aioimaplib schedules `create_connection` as a fire-and-forget task; on a - refused connection it raises and asyncio logs a noisy "Task exception was - never retrieved" traceback unless retrieved here first. - """ + """tear down a half-built IMAP4 without leaking its fire-and-forget connect task (avoids an + asyncio "Task exception was never retrieved" traceback)""" task = getattr(mail, "_client_task", None) if task is not None and not task.done(): task.cancel() if task is not None: - # shield distinguishes our own task.cancel() from an external cancel of - # this coroutine: a bare `await task` swallowed both, resisting - # cancellation. under shield, CancelledError here means external only. + # shield: a bare `await task` would also swallow an external cancel; under + # shield, CancelledError here means external only. try: await asyncio.shield(task) except asyncio.CancelledError: @@ -148,13 +138,8 @@ class IMAPClient: self._selected_folder = None async def ensure_connection(self) -> bool: - """return a live, SELECTED-if-applicable connection, reconnecting if the link is stale - - re-selects the previously-selected folder after a reconnect; sequence-number - ids from before the reconnect are NOT valid against the new session (a fresh - SELECT can renumber the mailbox) unless use_uid=True. serialized by the - internal lock so a queued caller rechecks liveness before reconnecting. - """ + """return a live, SELECTED-if-applicable connection, reconnecting (and re-selecting the prior + folder) if the link is stale; see module docstring for the sequence-number-vs-use_uid caveat""" async with self._lock: if self._mail is not None: try: diff --git a/src/aiomail/extract.py b/src/aiomail/extract.py index 71c432a..4744aee 100644 --- a/src/aiomail/extract.py +++ b/src/aiomail/extract.py @@ -1,9 +1,4 @@ -"""code extraction and dynamic matching for email messages, pure logic with no network IO. - -`extract_code` pulls a one-time code out of a message; `as_predicate` turns a -string / compiled regex / callable into a uniform match function for filtering -senders and subjects. -""" +"""code extraction and dynamic matching for email messages, pure logic with no network IO.""" import email.message import logging import re @@ -103,9 +98,8 @@ def extract_code( ) -> Optional[str]: """extract a one-time code from a message, subject first then body parts - `patterns` are regexes tried in order (first capturing group wins, else the - whole match); if none hit, a standalone digit run whose length is in - `lengths` is returned. both are parameters so callers tune per provider. + `patterns` are regexes tried in order (first capturing group wins, else the whole + match); if none hit, a standalone digit run whose length is in `lengths` is returned. """ compiled = _compile(patterns) length_set = set(lengths) diff --git a/src/aiomail/oauth.py b/src/aiomail/oauth.py index b3c1701..054035a 100644 --- a/src/aiomail/oauth.py +++ b/src/aiomail/oauth.py @@ -1,8 +1,6 @@ -"""optional OAuth2 token providers (refresh-token -> access-token) for `OAuth2Auth`, credentials always caller-supplied. - -aiohttp is an optional extra so the core stays light; missing it raises a clear -error only when a provider is instantiated, not on import. -""" +"""optional OAuth2 token providers (refresh-token -> access-token) for `OAuth2Auth`, credentials always +caller-supplied. aiohttp is an optional extra; missing it raises a clear error only when a provider is +instantiated, not on import.""" import asyncio import logging import time @@ -80,14 +78,13 @@ class _RefreshTokenProvider: if token: self._failures = 0 return token - # log a truncated body (never whole, may carry sensitive - # material) so a 200-with-no-token isn't a silent drop + # truncated, never whole: the body may carry sensitive material log.warning( "token endpoint %s -> 200 with no access_token: %s", endpoint, str(body_json)[:200], ) else: - # truncated only — the body may carry sensitive material + # truncated only - the body may carry sensitive material body = (await resp.text())[:200] log.warning("token endpoint %s -> %s: %s", endpoint, resp.status, body) except Exception as exc: diff --git a/src/aiomail/retrieve.py b/src/aiomail/retrieve.py index 85d0e6a..86fda50 100644 --- a/src/aiomail/retrieve.py +++ b/src/aiomail/retrieve.py @@ -1,8 +1,4 @@ -"""orchestration: `retrieve_otp` ties the client and extractor together to find the most recent valid OTP. - -sender/subject accept the flexible match specs from `extract`; provider quirks -(folders, age, patterns) live in the arguments, not hardcoded branches. -""" +"""orchestration: `retrieve_otp` ties the client and extractor together to find the most recent valid OTP.""" import asyncio import logging import time @@ -27,11 +23,10 @@ DEFAULT_FOLDERS: Sequence[str] = ("INBOX", "Junk", "Spam", "Archive", "All Mail" def _server_query(sender: MatchSpec, subject: MatchSpec, match_field: str = "from") -> str: - """build a narrowing IMAP query from plain-string specs only, falling back to ALL for regex/callable specs + """build a narrowing IMAP query from plain-string specs, falling back to ALL for regex/callable specs - `match_field="to"` searches TO OR FROM (a forwarded code may keep the - original From), matching the client-side forwarded-From fallback so the - server query never narrows out a result the client would have accepted. + `match_field="to"` searches TO OR FROM (a forwarded code may keep the original + From) so the server query never narrows out a result the client would accept. """ parts: List[str] = [] if isinstance(sender, str):