From bd9502c62d308dddc74eaaebdc9b835a7ad1b77a Mon Sep 17 00:00:00 2001 From: disqualifier Date: Fri, 3 Jul 2026 19:03:05 -0400 Subject: [PATCH] fix: safe_send routes embed= + fails loud on unknown kwargs; embed helpers deep-copy (no input mutation) Signed-off-by: disqualifier --- README.md | 7 +++++-- pyproject.toml | 2 +- src/dpy_commons/embeds.py | 9 +++++++-- src/dpy_commons/send.py | 18 ++++++++++++++++-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2dca92a..3f448fc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index b25e574..50782bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/src/dpy_commons/embeds.py b/src/dpy_commons/embeds.py index 7be3bc4..82109d1 100644 --- a/src/dpy_commons/embeds.py +++ b/src/dpy_commons/embeds.py @@ -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: diff --git a/src/dpy_commons/send.py b/src/dpy_commons/send.py index bd6f1f3..1e99e11 100644 --- a/src/dpy_commons/send.py +++ b/src/dpy_commons/send.py @@ -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