I wanted to send an error message to the user without access to context from a command. The error message sent should reply to a command with a discord message. This is also within a function with no discord objects and mainly deals with data.
The way I had been doing this was by raising a CommandError
raise CommandError(message, logger)
and handling this by on_command_error
async def on_command_error(self, ctx, error):
...
if isinstance(error, commands.CommandError):
# Internal error called to kill a command call due to error
try:
message, logger = error.args
except (TypeError, ValueError):
message = error
logger = self.logger
else:
logger = logger or self.logger
logger.warning(message)
await ctx.send(f"{message}")
return
While this worked, it didn't if I also didn't want to kill the execution flow from the raise
.
The question then becomes what do you do about it?
You can pass the context variable into every function/method that needs it. This is fine for higher-level methods/classes but for things that mostly do data manipulation, it doesn't need to be there and is just to call an error. Feels clunky.
Temporarily storing the context variable somewhere doesn't work for multi-user/guild situations. In the past, I have gotten around this by having a guild-specific error channel, but this also doesn't work as I am opening the bot to more users in more channels. Is there a better way?
My solution: To access the frame stack. Every function in the chain in discord.py is invoked with
wrapper
invoke
Even if not explictly called with invoke. This means you can scrape the context (called ctx) from the stack
def _get_frame_context(self, logger=None):
"""Inspect the stack to find the context object from invoke
Potential to increase speed as inspect.stack() is expensive
"""
frame_stack = inspect.stack()
for frame_info in frame_stack:
if frame_info.function.lower() == "invoke":
context = frame_info.frame.f_locals["ctx"]
break
else:
logger = self.logger if not logger else logger
raise CommandError("Could not find context object", logger)
del frame_stack # to not leak frames
return context
async def send_error(self, message: str, logger: logging.Logger, context=None):
"""
:param message: The error message
:param logger: The logger from where the error occured
Send a message to the invocation channel, without disrupting execution,
assuming find context
"""
if context is None:
try:
context = await self._get_frame_context()
except CommandError:
raise CommandError(f"Could not find context object:\n{message}", logger)
await context.send(message)
logger.warning(message)
Note: inspect.stack() is slow and expensive so for larger calls, there are better options