diff --git a/README.md b/README.md index 3766a66..e2d74fd 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@ live from `bot.settings` so it can change at runtime via a command. `requirements.txt`: ``` -dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.4 +dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.5 ``` Direct: ```bash -pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.4" +pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.5" ``` Requires `discord.py` (pulled transitively). @@ -72,13 +72,15 @@ await bot.log.debug("noisy", log_to_file=False) # -> Discord only ## Errors -Resolution failures (unresolvable guild/channel, bad config, a non-text channel) raise -`ValueError` from `initialize()` — a misconfigured logger should fail loudly at setup. -Underlying `discord` exceptions (`NotFound` / `Forbidden` / `HTTPException`) from -`fetch_guild`/`fetch_channel` are normalized to that `ValueError` so callers see one -error type. On a **per-call** send, neither resolution nor send failures propagate: they -fall back to the stdlib logger so a transient Discord failure (or a per-call `guild=` -that doesn't resolve) never breaks the caller's command. +Resolution failures (unresolvable guild/channel, bad config, a non-text channel — whether +passed as an id or as an already-resolved object) raise `ValueError` from `initialize()` — +a misconfigured logger should fail loudly at setup. Underlying `discord` exceptions +(`NotFound` / `Forbidden` / `HTTPException` / `InvalidData` and other `ClientException` +subclasses) from `fetch_guild`/`fetch_channel` are normalized to that `ValueError` on every +resolution path (construction channel, per-guild settings lookup) so callers see one error +type. On a **per-call** send, neither resolution nor send failures propagate: they fall +back to the stdlib logger so a transient Discord failure (or a per-call `guild=` that +doesn't resolve) never breaks the caller's command. ## Construction contract diff --git a/pyproject.toml b/pyproject.toml index e1ea0a4..143bba2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "dpy_logger" -version = "0.1.4" +version = "0.1.5" description = "Leveled Discord channel logger for discord.py — config-free, injectable, installable." requires-python = ">=3.10" dependencies = [ diff --git a/src/dpy_logger/dpy_logger.py b/src/dpy_logger/dpy_logger.py index 815963e..6bd65d3 100644 --- a/src/dpy_logger/dpy_logger.py +++ b/src/dpy_logger/dpy_logger.py @@ -40,11 +40,15 @@ happens before the discord send, so the record survives even if discord fails. extending: this base has no feed/announcement method by design. a project that wants one subclasses DPYLogger and adds it, reusing _resolve. -errors: resolution failures (unresolvable guild/channel, bad config) raise +errors: resolution failures (unresolvable guild/channel, bad config, a +non-text channel passed as either an id or an already-resolved object) raise ValueError from `initialize()` — a misconfigured logger should fail loudly at -setup. on a per-call send, resolution AND send failures do NOT propagate: they -fall back to the stdlib logger so a transient discord failure (or a per-call -`guild=` that doesn't resolve) never breaks the caller's command. +setup. underlying discord exceptions (NotFound/Forbidden/HTTPException/ +InvalidData and other ClientException subclasses) are normalized to that +ValueError on every resolution path. on a per-call send, resolution AND send +failures do NOT propagate: they fall back to the stdlib logger so a transient +discord failure (or a per-call `guild=` that doesn't resolve) never breaks the +caller's command. """ import logging @@ -119,6 +123,12 @@ class DPYLogger: self.guild = self.channel.guild if self.guild is None and self.channel is not None: raise ValueError("[dpy_logger] channel provided without a resolvable guild") + # a resolved wrong-type object (Thread/VoiceChannel/ForumChannel) would also pass + # setup silently — _get_channel only recognizes TextChannel/int, so anything else + # falls through to the bot.settings lookup at send time and misroutes or blackholes + # the sink. fail loud here, mirroring the int-path check in _get_channel. + if self.channel is not None and not isinstance(self.channel, (discord.TextChannel, int)): + raise ValueError(f"[dpy_logger] channel {self.channel!r} is not a text channel") async def _get_guild(self, guild): """resolve a guild from id-or-object, raising if unresolvable""" @@ -147,7 +157,14 @@ class DPYLogger: if isinstance(self.channel, discord.TextChannel): return self.channel if isinstance(self.channel, int): - channel = await guild.fetch_channel(self.channel) + # fetch_channel raises NotFound/Forbidden/HTTPException/InvalidData (a + # ClientException, not an HTTPException); normalize all of those to the + # lib's ValueError so a bad construction channel id fails loud with one + # error type, matching the settings path below + try: + channel = await guild.fetch_channel(self.channel) + except (discord.HTTPException, discord.ClientException) as error: + raise ValueError(f"[dpy_logger] could not fetch channel {self.channel}: {error}") from error if not isinstance(channel, discord.TextChannel): # fetch_channel can return a Voice/Category/Forum channel; fail loud # at setup like the settings path, not later via an AttributeError on .send @@ -160,8 +177,9 @@ class DPYLogger: raise ValueError(f"[dpy_logger] no log channel configured for guild {guild.id}") try: channel = await guild.fetch_channel(channel_id) - except discord.HTTPException as error: - # fetch_channel raises NotFound/Forbidden/HTTPException; normalize to the + except (discord.HTTPException, discord.ClientException) as error: + # fetch_channel raises NotFound/Forbidden/HTTPException/InvalidData (a + # ClientException, not an HTTPException); normalize all of those to the # lib's ValueError so a bad configured id fails loud with one error type raise ValueError(f"[dpy_logger] could not fetch channel {channel_id}: {error}") from error if not isinstance(channel, discord.TextChannel):