fix: safe_send routes embed= + fails loud on unknown kwargs; embed helpers deep-copy (no input mutation)

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-03 19:03:05 -04:00
parent 5eb9ed2be0
commit bd9502c62d
4 changed files with 29 additions and 7 deletions
+5 -2
View File
@@ -7,7 +7,7 @@ text chunking, timestamp helpers, interactive await-prompts, and a limit-safe se
## Install ## Install
``` ```
dpy_commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_commons.git@v0.1.2 dpy_commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_commons.git@v0.1.3
``` ```
## Usage ## Usage
@@ -91,7 +91,10 @@ apply to **every** chunked message, not just the first, so a suppressed `@everyo
stays suppressed across the whole split. Once-only kwargs (`file`, `files`, `stickers`, stays suppressed across the whole split. Once-only kwargs (`file`, `files`, `stickers`,
`nonce`, `reference`, `mention_author`, `view`, `poll`, `delete_after`) still ride the first `nonce`, `reference`, `mention_author`, `view`, `poll`, `delete_after`) still ride the first
message only. A bare `safe_send(destination)` with no content/embeds sends a single message message only. A bare `safe_send(destination)` with no content/embeds sends a single message
with `content=None`. with `content=None`. A singular `embed=` kwarg is folded into the `embeds` pipeline (fit +
split like any other embed); passing both `embed` and `embeds` raises `TypeError`, matching
discord.py's own `Messageable.send` rule. Any kwarg `safe_send` doesn't recognize also raises
`TypeError` naming it, rather than being silently dropped.
## Notes / deviations ## Notes / deviations
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "dpy_commons" name = "dpy_commons"
version = "0.1.2" version = "0.1.3"
description = "Shared discord.py utilities — message/embed parsing, limit-fitting, link extraction, chunking, timestamps, await-prompts, limit-safe send. Config-free, installable." description = "Shared discord.py utilities — message/embed parsing, limit-fitting, link extraction, chunking, timestamps, await-prompts, limit-safe send. Config-free, installable."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
+7 -2
View File
@@ -2,6 +2,7 @@
and split a list into sendable groups; none of these mutate their input""" and split a list into sendable groups; none of these mutate their input"""
from __future__ import annotations from __future__ import annotations
import copy
import logging import logging
import discord import discord
@@ -25,8 +26,12 @@ _ZWSP = ""
def _copy(embed: discord.Embed) -> discord.Embed: def _copy(embed: discord.Embed) -> discord.Embed:
"""deep copy via dict round-trip so the input is never mutated""" """deep copy via dict round-trip so the input is never mutated
return discord.Embed.from_dict(embed.to_dict())
discord.py 2.7.1's to_dict() reuses the embed's own _fields list rather than copying
it, so from_dict(embed.to_dict()) would still share that list with the input; the
explicit deepcopy breaks that aliasing"""
return discord.Embed.from_dict(copy.deepcopy(embed.to_dict()))
def fit_embed(embed: discord.Embed) -> discord.Embed: def fit_embed(embed: discord.Embed) -> discord.Embed:
+16 -2
View File
@@ -34,14 +34,28 @@ async def safe_send(
`view`, `poll`, `delete_after`) ride only on the FIRST message so they aren't duplicated `view`, `poll`, `delete_after`) ride only on the FIRST message so they aren't duplicated
across the split across the split
a singular `embed=` kwarg is folded into the `embeds` pipeline (fit + split like any
other embed); passing both `embed` and `embeds` raises `TypeError`, matching
discord.py's own `Messageable.send` rule. Any other kwarg not recognized by either
whitelist above also raises `TypeError` naming it, instead of being silently dropped
with no content and no embeds, one message is still sent (content=None) so a bare with no content and no embeds, one message is still sent (content=None) so a bare
safe_send(destination) or a files/view-only call goes out as a single message rather than safe_send(destination) or a files/view-only call goes out as a single message rather than
silently doing nothing""" silently doing nothing"""
content_chunks = chunk_text(content) if content else [] embed = kwargs.pop("embed", None)
embed_groups = split_embeds([fit_embed(embed) for embed in embeds]) if embeds else [] if embed is not None:
if embeds is not None:
raise TypeError("safe_send() cannot mix embed and embeds")
embeds = [embed]
per_chunk = {k: v for k, v in kwargs.items() if k in _PER_CHUNK_KWARGS} per_chunk = {k: v for k, v in kwargs.items() if k in _PER_CHUNK_KWARGS}
once_only = {k: v for k, v in kwargs.items() if k in _ONCE_ONLY_KWARGS} once_only = {k: v for k, v in kwargs.items() if k in _ONCE_ONLY_KWARGS}
unexpected = [k for k in kwargs if k not in _PER_CHUNK_KWARGS and k not in _ONCE_ONLY_KWARGS]
if unexpected:
raise TypeError(f"safe_send() got unexpected keyword argument(s): {', '.join(unexpected)}")
content_chunks = chunk_text(content) if content else []
embed_groups = split_embeds([fit_embed(e) for e in embeds]) if embeds else []
messages: "list[discord.Message]" = [] messages: "list[discord.Message]" = []
first = True first = True