1 Commits
Author SHA1 Message Date
dsql 1ea1cb5b63 fix: falsy details/actor render + critical() delegates to _send (dpylogger-2/3)
- dpylogger-2: build_embed/_emit_stdlib test 'is not None' so falsy-but-valid values (0,
  False) render; only None or '' is dropped/substituted.
- dpylogger-3: critical() delegates to _send (via a new content= param) instead of
  hand-mirroring the send/error scaffolding — single source of truth.
verified vs discord.py 2.7.1. bump v0.1.2 -> v0.1.4

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 00:28:36 -04:00
3 changed files with 26 additions and 22 deletions
+3 -3
View File
@@ -10,18 +10,18 @@ live from `bot.settings` so it can change at runtime via a command.
`requirements.txt`: `requirements.txt`:
``` ```
dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.3 dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.4
``` ```
Direct: Direct:
```bash ```bash
pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.3" pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.4"
``` ```
Requires `discord.py` (pulled transitively). Requires `discord.py` (pulled transitively).
Drop the `@v0.1.3` suffix from the line above to install the latest unpinned. Drop the `@v0.1.4` suffix from the line above to install the latest unpinned.
## Usage ## Usage
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "dpy_logger" name = "dpy_logger"
version = "0.1.3" version = "0.1.4"
description = "Leveled Discord channel logger for discord.py — config-free, injectable, installable." description = "Leveled Discord channel logger for discord.py — config-free, injectable, installable."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
+22 -18
View File
@@ -179,14 +179,16 @@ class DPYLogger:
if self._embed_builder is not None: if self._embed_builder is not None:
return self._embed_builder(self, level, action, actor, details) return self._embed_builder(self, level, action, actor, details)
em = discord.Embed(color=self.colors[level]) em = discord.Embed(color=self.colors[level])
if action: # test `is not None` so a falsy-but-valid value (0, False) still renders; only a
# genuinely absent field (None) or an empty string is dropped/substituted below
if action is not None and str(action) != "":
em.add_field(name="Action", value=f"`{action}`", inline=True) em.add_field(name="Action", value=f"`{action}`", inline=True)
if actor: if actor is not None and str(actor) != "":
em.add_field(name="Actor", value=f"`{actor}`", inline=True) em.add_field(name="Actor", value=f"`{actor}`", inline=True)
# discord rejects an empty field value (50035) and truncates nothing itself, so # discord rejects an empty field value (50035) and truncates nothing itself, so
# an empty or >1024-char message would 400 the send; substitute + cap to keep # an empty or >1024-char message would 400 the send; substitute + cap to keep
# every logging call producing a valid embed # every logging call producing a valid embed
log_value = str(details) if details else "(no message)" log_value = str(details) if details is not None and str(details) != "" else "(no message)"
if len(log_value) > 1024: if len(log_value) > 1024:
log_value = log_value[:1021] + "..." log_value = log_value[:1021] + "..."
em.add_field(name="Log", value=log_value, inline=False) em.add_field(name="Log", value=log_value, inline=False)
@@ -200,20 +202,28 @@ class DPYLogger:
def _emit_stdlib(self, level, action, actor, log_msg): def _emit_stdlib(self, level, action, actor, log_msg):
"""mirror the log line to the stdlib logger (routed to file/console by the app)""" """mirror the log line to the stdlib logger (routed to file/console by the app)"""
parts = [p for p in (action, str(actor) if actor else None, log_msg) if p] # render falsy-but-valid parts (0, False); drop only genuinely-absent (None) ones
raw = (action, str(actor) if actor is not None else None, log_msg)
parts = [str(p) for p in raw if p is not None and str(p) != ""]
_log.log(LEVEL_MAP.get(level, logging.INFO), f"[{level}] " + " | ".join(parts)) _log.log(LEVEL_MAP.get(level, logging.INFO), f"[{level}] " + " | ".join(parts))
async def _send(self, level, log_msg, action=None, actor=None, guild=None, log_to_file=None): async def _send(self, level, log_msg, action=None, actor=None, guild=None,
log_to_file=None, content=None):
"""resolve channel and dispatch a leveled embed; mirror to stdlib unless opted out """resolve channel and dispatch a leveled embed; mirror to stdlib unless opted out
stdlib emit happens first so the record survives even if the discord send fails. stdlib emit happens first so the record survives even if the discord send fails.
a failed send falls back to stdlib rather than propagating into the caller. a failed send falls back to stdlib rather than propagating into the caller.
`content` carries the critical-level ping/@here text (None for the other levels),
so every level shares this one send/error contract.
""" """
if self.log_to_file if log_to_file is None else log_to_file: if self.log_to_file if log_to_file is None else log_to_file:
self._emit_stdlib(level, action, actor, log_msg) self._emit_stdlib(level, action, actor, log_msg)
try: try:
channel = await self._resolve(guild) channel = await self._resolve(guild)
return await channel.send(embed=self.build_embed(level, action, actor, log_msg)) return await channel.send(
content=content,
embed=self.build_embed(level, action, actor, log_msg),
)
except Exception: except Exception:
_log.exception(f"[dpy_logger] failed to send {level} log: {log_msg}") _log.exception(f"[dpy_logger] failed to send {level} log: {log_msg}")
return None return None
@@ -245,21 +255,15 @@ class DPYLogger:
return await self._send("task", log, action, "SYSTEM/TASK", guild, log_to_file) return await self._send("task", log, action, "SYSTEM/TASK", guild, log_to_file)
async def critical(self, log, action=None, actor=None, guild=None, log_to_file=None): async def critical(self, log, action=None, actor=None, guild=None, log_to_file=None):
"""log a critical-level message and ping configured devs""" """log a critical-level message and ping configured devs
if self.log_to_file if log_to_file is None else log_to_file:
self._emit_stdlib("critical", action, actor, log) delegates to _send with the ping/@here content so the send + error contract has a
single source of truth; critical only adds the alert `content`.
"""
if self.pings: if self.pings:
content = "alert: " + " ".join(f"<@{u}>" for u in self.pings) content = "alert: " + " ".join(f"<@{u}>" for u in self.pings)
elif self.alert_here: elif self.alert_here:
content = "alert: @here" content = "alert: @here"
else: else:
content = None content = None
try: return await self._send("critical", log, action, actor, guild, log_to_file, content)
channel = await self._resolve(guild)
return await channel.send(
content=content,
embed=self.build_embed("critical", action, actor, log),
)
except Exception:
_log.exception(f"[dpy_logger] failed to send critical log: {log}")
return None