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
```
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
@@ -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`,
`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
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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
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."
requires-python = ">=3.10"
dependencies = [
+7 -2
View File
@@ -2,6 +2,7 @@
and split a list into sendable groups; none of these mutate their input"""
from __future__ import annotations
import copy
import logging
import discord
@@ -25,8 +26,12 @@ _ZWSP = ""
def _copy(embed: discord.Embed) -> discord.Embed:
"""deep copy via dict round-trip so the input is never mutated"""
return discord.Embed.from_dict(embed.to_dict())
"""deep copy via dict round-trip so the input is never mutated
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:
+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
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
safe_send(destination) or a files/view-only call goes out as a single message rather than
silently doing nothing"""
content_chunks = chunk_text(content) if content else []
embed_groups = split_embeds([fit_embed(embed) for embed in embeds]) if embeds else []
embed = kwargs.pop("embed", None)
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}
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]" = []
first = True