modmail-dev / Modmail

A Discord bot that functions as a shared inbox between staff and members, similar to Reddit's Modmail.
https://docs.modmail.dev
GNU Affero General Public License v3.0
1.57k stars 4.59k forks source link

[BUG]: Updating or reloading plugins does not reload imported files. #3223

Closed Jerrie-Aries closed 9 months ago

Jerrie-Aries commented 1 year ago

Bot Version

v4.0.1

How are you hosting Modmail?

Other

Error Logs

None

Screenshots

No response

Additional Information

This only occurs on plugins that use multiple files. More in-depth explanations can be found on discord.py discussions, here.

This happened to me quite a few times now since most of my plugins use multiple files. Updating the plugins does not re-import other files in same directory or sub-directories.

Steps to produce:

Jerrie-Aries commented 1 year ago

Side note: Internally when unloading an extension, discord.py has already done the unloading sub-modules thing here, specifically in Bot._call_module_finalizers(). However that will only work if the entry point (where async def setup() is) of the extension is in __init__.py file, which means all the files and directories in the same folder as the __init__.py will be considered as the sub-modules of the extension and will automatically be unloaded when unloading the extension. Example: ├── myplugin │ ├── __init__.py │ ├── views.py │ ├── models.py Here the extension string would be something like plugins.myplugin.

Since the file structure for modmail plugins is quite different, and the entry point is not in __init__.py, files and directories in the same folder as the main plugin file (e.g. myplugin.py) will not be unloaded when unloading the plugin. Example: ├── myplugin │ ├── myplugin.py │ ├── views.py │ ├── models.py Here the extension string would be something like plugins.myplugin.myplugin.

Luckily, unloading the modules or files in plugin's directory can be done manually, whether in the core of the bot (cogs/plugins.py), or plugin developers can implement it themselves in their plugins. Possible solution, after unloading the extension do the cleanup:

# plugin here is the Plugin object
ext_parent = ".".join(plugin.ext_string.split(".")[:-1])
for module in list(sys.modules.keys()):
    if module == ext_parent or module.startswith(ext_parent + "."):
        del sys.modules[module]