Search code examples
pythondiscord.pypython-decorators

How to inject arguments from a custom decorator to a command in discord.py?


I'm working on a bot which keeps track of various text-based games in different channels. Commands used outside the channel in which the relevant game is running should do nothing of course, and neither should they activate is the game is not running (for example, when a new game is starting soon). Therefore, almost all my commands start with the same few lines of code

@commands.command()
async def example_command(self, ctx):
    game = self.game_manager.get_game(ctx.channel.id)
    if not game or game.state == GameState.FINISHED:
        return

I'd prefer to just decorate all these methods instead. Discord.py handily provides a system of "check" decorators to automate these kinds of checks, but this does not allow me to pass on the game object to the command. As every command needs a reference to this object, I'd have to retrieve it every time again anyway, and ideally I'd like to just pass it along to the command.

My naive attempt at a decorator looks as follows

def is_game_running(func):
    async def wrapper(self, ctx):
        # Retrieve `game` object here and do some checks
        game = ...

        return await func(self, ctx, game)

    wrapper.__name__ = func.__name__

    return wrapper

# Somewhere in the class
@commands.command()
@is_game_running
async def example_command(self, ctx, game):
    pass

However this gives me the quite cryptic error "discord.ext.commands.errors.MissingRequiredArgument: ctx is a required argument that is missing."

I've tried a few variants of this, using *args etc... but nothing seems to work.


Solution

  • You can keep the decorator outside of the class and handle the passing of the context object by using *args. In this case, args will be a tuple of size 2 that first contains the class instance and then the context object. You can thus access the context object by using args[1].

    For illustration purposes the below code saves each item of the args tuple into its own variable. This is not strictly needed since you could create a new 3 size tuple of (class instance, context, game) and pass that to func inside wrapper.

    import asyncio
    import discord
    from discord.ext import commands
    
    
    def is_game_running(func):
        async def wrapper(*args):
            # Retrieve `game` object here and do some checks
            class_instance = args[0]  # this is self from the class
            ctx = args[1]  # this is the context object
            
            game = ctx.channel.id
            
            # example_command requires variables: self, ctx, game
            return await func(class_instance, ctx, game)
    
        wrapper.__name__ = func.__name__
    
        return wrapper
    
    
    class Test(commands.Cog):
        def __init__(self, bot):
            self.bot = bot
    
        # Somewhere in the class
        @commands.command()
        @is_game_running
        async def example_command(self, ctx, game):
            await ctx.send(game)
    
    
    client = commands.Bot(intents=discord.Intents.all(), command_prefix='!')
    
    
    async def main():
        async with client:
            await client.add_cog(Test(client))
            await client.start('token')
    
    asyncio.run(main())