From 94b25d51ceb13a84ea62789145d5c1d9e7c7bf0f Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 6 Jul 2026 00:15:11 -0400 Subject: [PATCH] docs: annotate send/send_any as Optional[WebhookMessage], document wait=True (dpywebhooks-2) Both were annotated -> discord.WebhookMessage and README-documented that way, but discord.py's Webhook.send defaults wait=False and returns None unless the caller passes wait=True - so the annotation overpromised and a consumer chaining .id off the result without knowing to pass wait=True would get an AttributeError. Widen both annotations to Optional and add a one-line note (module docstring + README) on the wait=True requirement. Signed-off-by: disqualifier --- README.md | 13 +++++++++---- src/dpy_webhooks/dpy_webhooks.py | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 18578b7..ba2a3a4 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ await hooks.send(channel, content="hi", embeds=[e]) # get-or-create + send wh = await hooks.get_or_create(channel) # reuse the webhook directly ``` +`send`/`send_any` forward `wait` straight to discord.py's `Webhook.send`, which defaults +`wait=False` and returns `None` in that case; pass `wait=True` to get back the sent +`discord.WebhookMessage` (e.g. to read `.id` or edit it later). + With a persistence store so webhooks survive a restart: ```python @@ -72,14 +76,15 @@ The module docstring (`help(dpy_webhooks)` / IDE hover) is the source of truth. - `get_or_create(channel, *, evict_oldest=False) -> discord.Webhook` - `get(channel) -> discord.Webhook | None` — cache then store; never creates -- `send(channel, **kwargs) -> discord.WebhookMessage` — get-or-create then send, with - self-heal +- `send(channel, **kwargs) -> discord.WebhookMessage | None` — get-or-create then send, with + self-heal; `None` unless `wait=True` - `clear(channel) -> None` — delete server-side + store record (idempotent) - `count(channel) -> int` — webhooks currently on the channel - `list(channel) -> list[discord.Webhook]` — all live webhooks on the channel (no create) - `pick(channel, *, strategy="first") -> discord.Webhook | None` — select an existing one -- `send_any(channel, *, strategy="round_robin", **kwargs) -> discord.WebhookMessage` — send - via a picked existing webhook, falling back to `get_or_create` +- `send_any(channel, *, strategy="round_robin", **kwargs) -> discord.WebhookMessage | None` — + send via a picked existing webhook, falling back to `get_or_create`; `None` unless + `wait=True` **Self-heal.** If `send` hits a dead webhook (deleted server-side / invalid token), the lib clears the record, recreates the webhook, and retries the send **once**; a second failure diff --git a/src/dpy_webhooks/dpy_webhooks.py b/src/dpy_webhooks/dpy_webhooks.py index aedbebb..27799b2 100644 --- a/src/dpy_webhooks/dpy_webhooks.py +++ b/src/dpy_webhooks/dpy_webhooks.py @@ -38,7 +38,9 @@ then raises loud. a retry carrying ``file=``/``files=`` rebuilds each ``discord. its source first, since discord.py closes a File's handle after the first send attempt and resending the same object would otherwise upload it as 0 bytes; a File that can't be safely rebuilt raises ``ValueError`` instead. store failures propagate — the store owns its own -durability. +durability. ``send``/``send_any`` forward ``wait`` straight to discord.py's +``Webhook.send``, which defaults ``wait=False`` and returns ``None`` in that case; pass +``wait=True`` to get back the sent :class:`discord.WebhookMessage`. discover / select / rotate -------------------------- @@ -267,10 +269,13 @@ class DPYWebhooks: *, strategy: str = "round_robin", **kwargs: Any, - ) -> discord.WebhookMessage: + ) -> "Optional[discord.WebhookMessage]": """send via a picked EXISTING webhook, falling back to get_or_create when the channel has none; on a dead pick, deletes that dead webhook directly (clear() only when the - pick IS the managed webhook) then recreates + retries the send once""" + pick IS the managed webhook) then recreates + retries the send once + + returns None unless kwargs includes wait=True, matching discord.py's own + Webhook.send default (wait=False discards the response and returns None)""" webhook = await self.pick(channel, strategy=strategy) picked_managed = webhook is None if webhook is None: @@ -313,9 +318,12 @@ class DPYWebhooks: self._rr[channel_id] = state return webhook - async def send(self, channel: discord.TextChannel, **kwargs: Any) -> discord.WebhookMessage: + async def send(self, channel: discord.TextChannel, **kwargs: Any) -> "Optional[discord.WebhookMessage]": """get_or_create then send; on a dead-webhook failure, clear + recreate + retry once, - then raise""" + then raise + + returns None unless kwargs includes wait=True, matching discord.py's own + Webhook.send default (wait=False discards the response and returns None)""" webhook = await self.get_or_create(channel) try: return await webhook.send(**kwargs)