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 <dev@disqualifier.me>
This commit is contained in:
2026-07-06 00:15:11 -04:00
parent ee3c38972b
commit 94b25d51ce
2 changed files with 22 additions and 9 deletions
+9 -4
View File
@@ -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 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: With a persistence store so webhooks survive a restart:
```python ```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_or_create(channel, *, evict_oldest=False) -> discord.Webhook`
- `get(channel) -> discord.Webhook | None` — cache then store; never creates - `get(channel) -> discord.Webhook | None` — cache then store; never creates
- `send(channel, **kwargs) -> discord.WebhookMessage` — get-or-create then send, with - `send(channel, **kwargs) -> discord.WebhookMessage | None` — get-or-create then send, with
self-heal self-heal; `None` unless `wait=True`
- `clear(channel) -> None` — delete server-side + store record (idempotent) - `clear(channel) -> None` — delete server-side + store record (idempotent)
- `count(channel) -> int` — webhooks currently on the channel - `count(channel) -> int` — webhooks currently on the channel
- `list(channel) -> list[discord.Webhook]` — all live webhooks on the channel (no create) - `list(channel) -> list[discord.Webhook]` — all live webhooks on the channel (no create)
- `pick(channel, *, strategy="first") -> discord.Webhook | None` — select an existing one - `pick(channel, *, strategy="first") -> discord.Webhook | None` — select an existing one
- `send_any(channel, *, strategy="round_robin", **kwargs) -> discord.WebhookMessage` send - `send_any(channel, *, strategy="round_robin", **kwargs) -> discord.WebhookMessage | None`
via a picked existing webhook, falling back to `get_or_create` 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 **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 clears the record, recreates the webhook, and retries the send **once**; a second failure
+13 -5
View File
@@ -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 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 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 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 discover / select / rotate
-------------------------- --------------------------
@@ -267,10 +269,13 @@ class DPYWebhooks:
*, *,
strategy: str = "round_robin", strategy: str = "round_robin",
**kwargs: Any, **kwargs: Any,
) -> discord.WebhookMessage: ) -> "Optional[discord.WebhookMessage]":
"""send via a picked EXISTING webhook, falling back to get_or_create when the channel """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 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) webhook = await self.pick(channel, strategy=strategy)
picked_managed = webhook is None picked_managed = webhook is None
if webhook is None: if webhook is None:
@@ -313,9 +318,12 @@ class DPYWebhooks:
self._rr[channel_id] = state self._rr[channel_id] = state
return webhook 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, """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) webhook = await self.get_or_create(channel)
try: try:
return await webhook.send(**kwargs) return await webhook.send(**kwargs)