Search code examples
pythondiscorddiscord.pyattributeerror

How can I prevent getting an AttributeError after reloading a cog?


I am working on a Discord bot which handles music requests among other things. Whenever I change stuff in a cog and then reload it, I get an error message like AttributeError: 'NoneType' object has no attribute 'XXXX' error for the particular command.

For example, starting with this code for the cog:

    @commands.command()
    @commands.guild_only()
    @commands.check(audio_playing)
    @commands.check(in_voice_channel)
    @commands.check(is_audio_requester)
    async def loop(self, ctx):
        """Activates/Deactivates the loop for a song."""
        state = self.get_state(ctx.guild)
        status = state.now_playing.toggle_loop()
        if status is None:
            return await ctx.send(
                embed=discord.Embed(title=":no_entry:  Unable to toggle loop.", color=discord.Color.red()))
        else:
            return await ctx.send(embed=discord.Embed(
                title=f":repeat:  Loop: {'**Enabled**' if state.now_playing.loop else '**Disabled**'}.",
                color=self.bot.get_embed_color(ctx.guild)))

which calls a helper method get_state:

    def get_state(self, guild):
        """Gets the state for `guild`, creating it if it does not exist."""
        if guild.id in self.states:
            return self.states[guild.id]
        else:
            self.states[guild.id] = GuildState()
            return self.states[guild.id]

which creates an instance of this GuildState class:

class GuildState:
    """Helper class managing per-guild state."""

    def __init__(self):
        self.volume = 1.0
        self.playlist = []
        self.message_queue = []
        self.skip_votes = set()
        self.now_playing = None
        self.control_message = None
        self.loop = False
        self.skipped = False

    def is_requester(self, user):
        return self.now_playing.requested_by == user

Then I use code like this to make the bot respond to the command play URL by joining a voice channel:

if not state.now_playing:
    self._play_song(client, state, video)

where _play_song does:

    def _play_song(self, client, state, song):
        state.now_playing = song
    # Other things are not relevant

Now if I try to reload the cog, the next time loop is called I get an error like:

In loop:
  File "C:\Users\Dominik\PycharmProjects\AlchiReWrite\venv\lib\site-packages\discord\ext\commands\core.py", line 85, in wrapped
    ret = await coro(*args, **kwargs)
  File "C:\Users\Dominik\PycharmProjects\AlchiReWrite\cogs\music.py", line 220, in loop
    status = state.now_playing.toggle_loop()
AttributeError: 'NoneType' object has no attribute 'toggle_loop'

I tried using exception handling with try/except AttributeError to ignore the error, but it didn't resolve the problem.

Why does this occur, and how can I fix or prevent the problem? I query state for every command which has to do with music, is it maybe related to that?


Solution

  • When you reload the cog, the states dictionary in your cog will be empty. With state = self.get_state(ctx.guild), a new GuildState object is created. From the __init__ function of the GuildState class, self.now_playing is set to None.

    Because of this, status = state.now_playing.toggle_loop() will throw an AttributeError as None has no attributes (in this case, no toggle_loop attribute).

    If you want to get rid of these errors, you will need to set self.now_playing correctly to something that does have the needed attributes.

    If you want to keep the states dictionary as is, you can save it before reloading your cog and restore it. The below example assumes that the cog class is named TestCog.

    @client.command()
    async def reload(ctx):
        temp = client.get_cog('TestCog').states
        client.reload_extension('cog')
        client.get_cog('TestCog').states = temp
    

    Note that this may break your cog if you change how GuildState is created, as you are restoring the previous version of states.