11import asyncio
2- import glob
32import importlib
43import inspect
54import logging
6- import os
75import re
6+ import sys
7+ import time
88import warnings
99from collections import defaultdict
1010from functools import partial
1111from itertools import chain
1212from operator import attrgetter
13+ from pathlib import Path
14+ from weakref import WeakValueDictionary
1315
1416import sqlalchemy
15- import sys
16-
17- import time
1817
1918from cloudbot .event import Event , PostHookEvent
2019from cloudbot .hook import Priority , Action
@@ -92,6 +91,7 @@ def __init__(self, bot):
9291 self .bot = bot
9392
9493 self .plugins = {}
94+ self ._plugin_name_map = WeakValueDictionary ()
9595 self .commands = {}
9696 self .raw_triggers = {}
9797 self .catch_all_triggers = []
@@ -105,6 +105,14 @@ def __init__(self, bot):
105105 self .perm_hooks = defaultdict (list )
106106 self ._hook_waiting_queues = {}
107107
108+ def find_plugin (self , title ):
109+ """
110+ Finds a loaded plugin and returns its Plugin object
111+ :param title: the title of the plugin to find
112+ :return: The Plugin object if it exists, otherwise None
113+ """
114+ return self ._plugin_name_map .get (title )
115+
108116 @asyncio .coroutine
109117 def load_all (self , plugin_dir ):
110118 """
@@ -114,7 +122,10 @@ def load_all(self, plugin_dir):
114122
115123 :type plugin_dir: str
116124 """
117- path_list = glob .iglob (os .path .join (plugin_dir , '*.py' ))
125+ plugin_dir = Path (plugin_dir )
126+ # Load all .py files in the plugins directory and any subdirectory
127+ # But ignore files starting with _
128+ path_list = plugin_dir .rglob ("[!_]*.py" )
118129 # Load plugins asynchronously :O
119130 yield from asyncio .gather (* [self .load_plugin (path ) for path in path_list ], loop = self .bot .loop )
120131
@@ -131,27 +142,30 @@ def load_plugin(self, path):
131142
132143 Won't load any plugins listed in "disabled_plugins".
133144
134- :type path: str
145+ :type path: str | Path
135146 """
136147
137- file_path = os .path .abspath (path )
138- file_name = os .path .basename (path )
139- title = os .path .splitext (file_name )[0 ]
148+ path = Path (path )
149+ file_path = path .resolve ()
150+ file_name = file_path .name
151+ # Resolve the path relative to the current directory
152+ plugin_path = file_path .relative_to (self .bot .base_dir )
153+ title = '.' .join (plugin_path .parts [1 :]).rsplit ('.' , 1 )[0 ]
140154
141155 if "plugin_loading" in self .bot .config :
142156 pl = self .bot .config .get ("plugin_loading" )
143157
144158 if pl .get ("use_whitelist" , False ):
145159 if title not in pl .get ("whitelist" , []):
146- logger .info ('Not loading plugin module "{}": plugin not whitelisted' .format (file_name ))
160+ logger .info ('Not loading plugin module "{}": plugin not whitelisted' .format (title ))
147161 return
148162 else :
149163 if title in pl .get ("blacklist" , []):
150- logger .info ('Not loading plugin module "{}": plugin blacklisted' .format (file_name ))
164+ logger .info ('Not loading plugin module "{}": plugin blacklisted' .format (title ))
151165 return
152166
153167 # make sure to unload the previously loaded plugin from this path, if it was loaded.
154- if file_name in self .plugins :
168+ if file_path in self .plugins :
155169 yield from self .unload_plugin (file_path )
156170
157171 module_name = "plugins.{}" .format (title )
@@ -161,11 +175,11 @@ def load_plugin(self, path):
161175 if hasattr (plugin_module , "_cloudbot_loaded" ):
162176 importlib .reload (plugin_module )
163177 except Exception :
164- logger .exception ("Error loading {}:" .format (file_name ))
178+ logger .exception ("Error loading {}:" .format (title ))
165179 return
166180
167181 # create the plugin
168- plugin = Plugin (file_path , file_name , title , plugin_module )
182+ plugin = Plugin (str ( file_path ) , file_name , title , plugin_module )
169183
170184 # proceed to register hooks
171185
@@ -182,7 +196,8 @@ def load_plugin(self, path):
182196 plugin .unregister_tables (self .bot )
183197 return
184198
185- self .plugins [plugin .file_name ] = plugin
199+ self .plugins [plugin .file_path ] = plugin
200+ self ._plugin_name_map [plugin .title ] = plugin
186201
187202 for on_cap_available_hook in plugin .hooks ["on_cap_available" ]:
188203 for cap in on_cap_available_hook .caps :
@@ -284,21 +299,18 @@ def unload_plugin(self, path):
284299
285300 Returns True if the plugin was unloaded, False if the plugin wasn't loaded in the first place.
286301
287- :type path: str
302+ :type path: str | Path
288303 :rtype: bool
289304 """
290- file_name = os .path .basename (path )
291- title = os .path .splitext (file_name )[0 ]
292- if "disabled_plugins" in self .bot .config and title in self .bot .config ['disabled_plugins' ]:
293- # this plugin hasn't been loaded, so no need to unload it
294- return False
305+ path = Path (path )
306+ file_path = path .resolve ()
295307
296308 # make sure this plugin is actually loaded
297- if not file_name in self .plugins :
309+ if str ( file_path ) not in self .plugins :
298310 return False
299311
300312 # get the loaded plugin
301- plugin = self .plugins [file_name ]
313+ plugin = self .plugins [str ( file_path ) ]
302314
303315 for task in plugin .tasks :
304316 task .cancel ()
@@ -377,10 +389,10 @@ def unload_plugin(self, path):
377389 plugin .unregister_tables (self .bot )
378390
379391 # remove last reference to plugin
380- del self .plugins [plugin .file_name ]
392+ del self .plugins [plugin .file_path ]
381393
382394 if self .bot .config .get ("logging" , {}).get ("show_plugin_loading" , True ):
383- logger .info ("Unloaded all plugins from {}.py " .format (plugin .title ))
395+ logger .info ("Unloaded all plugins from {}" .format (plugin .title ))
384396
385397 return True
386398
@@ -609,6 +621,8 @@ def __init__(self, filepath, filename, title, code):
609621 # we need to find tables for each plugin so that they can be unloaded from the global metadata when the
610622 # plugin is reloaded
611623 self .tables = find_tables (code )
624+ # Keep a reference to this in case another plugin needs to access it
625+ self .code = code
612626
613627 @asyncio .coroutine
614628 def create_tables (self , bot ):
0 commit comments