Skip to content

Commit 3bbfacc

Browse files
authored
Merge pull request CloudBotIRC#190 from linuxdaemon/gonzobot+recursive-plugin-loading
Allow plugins to be loaded from subdirectories
2 parents 382dc0f + 9ffe886 commit 3bbfacc

File tree

18 files changed

+52
-31
lines changed

18 files changed

+52
-31
lines changed

cloudbot/bot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import gc
88
from operator import attrgetter
9+
from pathlib import Path
910

1011
from sqlalchemy import create_engine
1112

@@ -59,6 +60,7 @@ class CloudBot:
5960

6061
def __init__(self, loop=asyncio.get_event_loop()):
6162
# basic variables
63+
self.base_dir = Path().resolve()
6264
self.loop = loop
6365
self.start_time = time.time()
6466
self.running = True

cloudbot/plugin.py

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import asyncio
2-
import glob
32
import importlib
43
import inspect
54
import logging
6-
import os
75
import re
6+
import sys
7+
import time
88
import warnings
99
from collections import defaultdict
1010
from functools import partial
1111
from itertools import chain
1212
from operator import attrgetter
13+
from pathlib import Path
14+
from weakref import WeakValueDictionary
1315

1416
import sqlalchemy
15-
import sys
16-
17-
import time
1817

1918
from cloudbot.event import Event, PostHookEvent
2019
from 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):

cloudbot/reloader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ def __init__(self, bot):
1616
self.observer = Observer()
1717
self.bot = bot
1818
self.reloading = set()
19-
self.event_handler = PluginEventHandler(self, patterns=["*.py"])
19+
self.event_handler = PluginEventHandler(self, patterns=["[!_]*.py"])
2020

2121
def start(self, module_path):
2222
"""Starts the plugin reloader
2323
:type module_path: str
2424
"""
25-
self.observer.schedule(self.event_handler, module_path, recursive=False)
25+
self.observer.schedule(self.event_handler, module_path, recursive=True)
2626
self.observer.start()
2727

2828
def stop(self):

plugins/core/__init__.py

Whitespace-only changes.
File renamed without changes.

0 commit comments

Comments
 (0)