diff --git a/earwigbot/__init__.py b/earwigbot/__init__.py index 1ebde74..39f0938 100644 --- a/earwigbot/__init__.py +++ b/earwigbot/__init__.py @@ -48,4 +48,5 @@ if not __release__: finally: del _add_git_commit_id_to_version -from earwigbot import blowfish, bot, commands, config, irc, tasks, util, wiki +from earwigbot import (blowfish, bot, commands, config, irc, managers, tasks, + util, wiki) diff --git a/earwigbot/bot.py b/earwigbot/bot.py index 8bf92bb..071cca2 100644 --- a/earwigbot/bot.py +++ b/earwigbot/bot.py @@ -24,11 +24,10 @@ import logging from threading import Lock, Thread from time import sleep, time -from earwigbot.commands import CommandManager from earwigbot.config import BotConfig from earwigbot.irc import Frontend, Watcher -from earwigbot.tasks import TaskManager -from earwigbot.wiki import SitesDBManager +from earwigbot.managers import CommandManager, TaskManager +from earwigbot.wiki import SitesDB __all__ = ["Bot"] @@ -58,7 +57,7 @@ class Bot(object): self.logger = logging.getLogger("earwigbot") self.commands = CommandManager(self) self.tasks = TaskManager(self) - self.wiki = SitesDBManager(self.config) + self.wiki = SitesDB(self.config) self.frontend = None self.watcher = None @@ -73,12 +72,12 @@ class Bot(object): if self.config.components.get("irc_frontend"): self.logger.info("Starting IRC frontend") self.frontend = Frontend(self) - Thread(name=name, target=self.frontend.loop).start() + Thread(name="irc_frontend", target=self.frontend.loop).start() if self.config.components.get("irc_watcher"): self.logger.info("Starting IRC watcher") self.watcher = Watcher(self) - Thread(name=name, target=self.watcher.loop).start() + Thread(name="irc_watcher", target=self.watcher.loop).start() def _start_wiki_scheduler(self): def wiki_scheduler(): @@ -92,7 +91,7 @@ class Bot(object): if self.config.components.get("wiki_scheduler"): self.logger.info("Starting wiki scheduler") - Thread(name=name, target=wiki_scheduler).start() + Thread(name="wiki_scheduler", target=wiki_scheduler).start() def _stop_irc_components(self): if self.frontend: diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index 98cebec..33604f1 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -21,20 +21,16 @@ # SOFTWARE. """ -EarwigBot's IRC Command Manager +EarwigBot's IRC Commands This package provides the IRC "commands" used by the bot's front-end component. This module contains the BaseCommand class (import with -`from earwigbot.commands import BaseCommand`) and an internal CommandManager -class. This can be accessed through `bot.commands`. +`from earwigbot.commands import BaseCommand`), whereas the package contains +various built-in commands. Additional commands can be installed as plugins in +the bot's working directory. """ -import imp -from os import listdir, path -from re import sub -from threading import Lock - -__all__ = ["BaseCommand", "CommandManager"] +__all__ = ["BaseCommand"] class BaseCommand(object): """A base class for commands on IRC. @@ -90,95 +86,3 @@ class BaseCommand(object): Note that """ pass - - -class CommandManager(object): - def __init__(self, bot): - self.bot = bot - self.logger = bot.logger.getChild("commands") - self._commands = {} - self._command_access_lock = Lock() - - def __iter__(self): - for name in self._commands: - yield name - - def _load_command(self, name, path): - """Load a specific command from a module, identified by name and path. - - We'll first try to import it using imp magic, and if that works, make - an instance of the 'Command' class inside (assuming it is an instance - of BaseCommand), add it to self._commands, and log the addition. Any - problems along the way will either be ignored or logged. - """ - f, path, desc = imp.find_module(name, [path]) - try: - module = imp.load_module(name, f, path, desc) - except Exception: - e = "Couldn't load module {0} from {1}" - self.logger.exception(e.format(name, path)) - return - finally: - f.close() - - try: - command_class = module.Command - except AttributeError: - return # No command in this module - try: - command = command_class(self.bot) - except Exception: - e = "Error initializing Command() class in {0} (from {1})" - self.logger.exception(e.format(name, path)) - return - if not isinstance(command, BaseCommand): - return - - self._commands[command.name] = command - self.logger.debug("Loaded command {0}".format(command.name)) - - def _load_directory(self, dir): - """Load all valid commands in a given directory.""" - processed = [] - for name in listdir(dir): - if not name.endswith(".py") and not name.endswith(".pyc"): - continue - if name.startswith("_") or name.startswith("."): - continue - modname = sub("\.pyc?$", "", name) # Remove extension - if modname not in processed: - self._load_command(modname, dir) - processed.append(modname) - - def load(self): - """Load (or reload) all valid commands into self._commands.""" - with self._command_access_lock: - self._commands.clear() - builtin_dir = path.dirname(__file__) - plugins_dir = path.join(self.bot.config.root_dir, "commands") - self._load_directory(builtin_dir) # Built-in commands - self._load_directory(plugins_dir) # Custom commands, aka plugins - - msg = "Loaded {0} commands: {1}" - commands = ", ".join(self._commands.keys()) - self.logger.info(msg.format(len(self._commands), commands)) - - def check(self, hook, data): - """Given an IRC event, check if there's anything we can respond to.""" - with self._command_access_lock: - for command in self._commands.values(): - if hook in command.hooks: - if command.check(data): - try: - command._wrap_process(data) - except Exception: - e = "Error executing command '{0}':" - self.logger.exception(e.format(data.command)) - break - - def get(self, command_name): - """Return the class instance associated with a certain command name. - - Will raise KeyError if the command is not found. - """ - return self._command[command_name] diff --git a/earwigbot/managers.py b/earwigbot/managers.py new file mode 100644 index 0000000..5df4e73 --- /dev/null +++ b/earwigbot/managers.py @@ -0,0 +1,250 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2012 by Ben Kurtovic +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import imp +from os import listdir, path +from re import sub +from threading import Lock, Thread +from time import gmtime, strftime + +from earwigbot.commands import BaseCommand +from earwigbot.tasks import BaseTask + +__all__ = ["CommandManager", "TaskManager"] + +class _BaseManager(object): + pass + + +class CommandManager(_BaseManager): + def __init__(self, bot): + self.bot = bot + self.logger = bot.logger.getChild("commands") + self._commands = {} + self._command_access_lock = Lock() + + def __iter__(self): + for name in self._commands: + yield name + + def _load_command(self, name, path): + """Load a specific command from a module, identified by name and path. + + We'll first try to import it using imp magic, and if that works, make + an instance of the 'Command' class inside (assuming it is an instance + of BaseCommand), add it to self._commands, and log the addition. Any + problems along the way will either be ignored or logged. + """ + f, path, desc = imp.find_module(name, [path]) + try: + module = imp.load_module(name, f, path, desc) + except Exception: + e = "Couldn't load module {0} from {1}" + self.logger.exception(e.format(name, path)) + return + finally: + f.close() + + try: + command_class = module.Command + except AttributeError: + return # No command in this module + try: + command = command_class(self.bot) + except Exception: + e = "Error initializing Command() class in {0} (from {1})" + self.logger.exception(e.format(name, path)) + return + if not isinstance(command, BaseCommand): + return + + self._commands[command.name] = command + self.logger.debug("Loaded command {0}".format(command.name)) + + def _load_directory(self, dir): + """Load all valid commands in a given directory.""" + processed = [] + for name in listdir(dir): + if not name.endswith(".py") and not name.endswith(".pyc"): + continue + if name.startswith("_") or name.startswith("."): + continue + modname = sub("\.pyc?$", "", name) # Remove extension + if modname not in processed: + self._load_command(modname, dir) + processed.append(modname) + + def load(self): + """Load (or reload) all valid commands into self._commands.""" + with self._command_access_lock: + self._commands.clear() + builtin_dir = path.join(path.dirname(__file__), "commands") + plugins_dir = path.join(self.bot.config.root_dir, "commands") + self._load_directory(builtin_dir) # Built-in commands + self._load_directory(plugins_dir) # Custom commands, aka plugins + + msg = "Loaded {0} commands: {1}" + commands = ", ".join(self._commands.keys()) + self.logger.info(msg.format(len(self._commands), commands)) + + def check(self, hook, data): + """Given an IRC event, check if there's anything we can respond to.""" + with self._command_access_lock: + for command in self._commands.values(): + if hook in command.hooks: + if command.check(data): + try: + command._wrap_process(data) + except Exception: + e = "Error executing command '{0}':" + self.logger.exception(e.format(data.command)) + break + + def get(self, command_name): + """Return the class instance associated with a certain command name. + + Will raise KeyError if the command is not found. + """ + return self._command[command_name] + + +class TaskManager(_BaseManager): + def __init__(self, bot): + self.bot = bot + self.logger = bot.logger.getChild("tasks") + self._tasks = {} + self._task_access_lock = Lock() + + def __iter__(self): + for name in self._tasks: + yield name + + def _wrapper(self, task, **kwargs): + """Wrapper for task classes: run the task and catch any errors.""" + try: + task.run(**kwargs) + except Exception: + msg = "Task '{0}' raised an exception and had to stop:" + self.logger.exception(msg.format(task.name)) + else: + msg = "Task '{0}' finished without error" + self.logger.info(msg.format(task.name)) + + def _load_task(self, name, path): + """Load a specific task from a module, identified by name and path. + + We'll first try to import it using imp magic, and if that works, make + an instance of the 'Task' class inside (assuming it is an instance of + BaseTask), add it to self._tasks, and log the addition. Any problems + along the way will either be ignored or logged. + """ + f, path, desc = imp.find_module(name, [path]) + try: + module = imp.load_module(name, f, path, desc) + except Exception: + e = "Couldn't load module {0} from {1}" + self.logger.exception(e.format(name, path)) + return + finally: + f.close() + + try: + task_class = module.Task + except AttributeError: + return # No task in this module + try: + task = task_class(self.bot) + except Exception: + e = "Error initializing Task() class in {0} (from {1})" + self.logger.exception(e.format(name, path)) + return + if not isinstance(task, BaseTask): + return + + self._tasks[task.name] = task + self.logger.debug("Loaded task {0}".format(task.name)) + + def _load_directory(self, dir): + """Load all valid tasks in a given directory.""" + processed = [] + for name in listdir(dir): + if not name.endswith(".py") and not name.endswith(".pyc"): + continue + if name.startswith("_") or name.startswith("."): + continue + modname = sub("\.pyc?$", "", name) # Remove extension + if modname not in processed: + self._load_task(modname, dir) + processed.append(modname) + + def load(self): + """Load (or reload) all valid tasks into self._tasks.""" + with self._task_access_lock: + self._tasks.clear() + builtin_dir = path.join(path.dirname(__file__), "tasks") + plugins_dir = path.join(self.bot.config.root_dir, "tasks") + self._load_directory(builtin_dir) # Built-in tasks + self._load_directory(plugins_dir) # Custom tasks, aka plugins + + msg = "Loaded {0} tasks: {1}" + tasks = ', '.join(self._tasks.keys()) + self.logger.info(msg.format(len(self._tasks), tasks)) + + def start(self, task_name, **kwargs): + """Start a given task in a new thread. kwargs are passed to task.run""" + msg = "Starting task '{0}' in a new thread" + self.logger.info(msg.format(task_name)) + + with self._task_access_lock: + try: + task = self._tasks[task_name] + except KeyError: + e = "Couldn't find task '{0}':" + self.logger.error(e.format(task_name)) + return + + task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs) + start_time = strftime("%b %d %H:%M:%S") + task_thread.name = "{0} ({1})".format(task_name, start_time) + task_thread.start() + + def schedule(self, now=None): + """Start all tasks that are supposed to be run at a given time.""" + if not now: + now = gmtime() + # Get list of tasks to run this turn: + tasks = self.bot.config.schedule(now.tm_min, now.tm_hour, now.tm_mday, + now.tm_mon, now.tm_wday) + + for task in tasks: + if isinstance(task, list): # They've specified kwargs, + self.start(task[0], **task[1]) # so pass those to start + else: # Otherwise, just pass task_name + self.start(task) + + def get(self, task_name): + """Return the class instance associated with a certain task name. + + Will raise KeyError if the task is not found. + """ + return self._tasks[task_name] diff --git a/earwigbot/tasks/__init__.py b/earwigbot/tasks/__init__.py index d70bcde..bfa7ef9 100644 --- a/earwigbot/tasks/__init__.py +++ b/earwigbot/tasks/__init__.py @@ -21,22 +21,20 @@ # SOFTWARE. """ -EarwigBot's Wiki Task Manager +EarwigBot's Bot Tasks This package provides the wiki bot "tasks" EarwigBot runs. This module contains -the BaseTask class (import with `from earwigbot.tasks import BaseTask`) and an -internal TaskManager class. This can be accessed through `bot.tasks`. -""" +the BaseTask class (import with `from earwigbot.tasks import BaseTask`), +whereas the package contains various built-in tasks. Additional tasks can be +installed as plugins in the bot's working directory. -import imp -from os import listdir, path -from re import sub -from threading import Lock, Thread -from time import gmtime, strftime +To run a task, use bot.tasks.start(name, **kwargs). **kwargs get passed to the +Task's run() function. +""" from earwigbot import wiki -__all__ = ["BaseTask", "TaskManager"] +__all__ = ["BaseTask"] class BaseTask(object): """A base class for bot tasks that edit Wikipedia.""" @@ -131,125 +129,3 @@ class BaseTask(object): self.logger.warn("Emergency task shutoff has been enabled!") return True - - -class TaskManager(object): - def __init__(self, bot): - self.bot = bot - self.logger = bot.logger.getChild("tasks") - self._tasks = {} - self._task_access_lock = Lock() - - def __iter__(self): - for name in self._tasks: - yield name - - def _wrapper(self, task, **kwargs): - """Wrapper for task classes: run the task and catch any errors.""" - try: - task.run(**kwargs) - except Exception: - msg = "Task '{0}' raised an exception and had to stop:" - self.logger.exception(msg.format(task.name)) - else: - msg = "Task '{0}' finished without error" - self.logger.info(msg.format(task.name)) - - def _load_task(self, name, path): - """Load a specific task from a module, identified by name and path. - - We'll first try to import it using imp magic, and if that works, make - an instance of the 'Task' class inside (assuming it is an instance of - BaseTask), add it to self._tasks, and log the addition. Any problems - along the way will either be ignored or logged. - """ - f, path, desc = imp.find_module(name, [path]) - try: - module = imp.load_module(name, f, path, desc) - except Exception: - e = "Couldn't load module {0} from {1}" - self.logger.exception(e.format(name, path)) - return - finally: - f.close() - - try: - task_class = module.Task - except AttributeError: - return # No task in this module - try: - task = task_class(self.bot) - except Exception: - e = "Error initializing Task() class in {0} (from {1})" - self.logger.exception(e.format(name, path)) - return - if not isinstance(task, BaseTask): - return - - self._tasks[task.name] = task - self.logger.debug("Loaded task {0}".format(task.name)) - - def _load_directory(self, dir): - """Load all valid tasks in a given directory.""" - processed = [] - for name in listdir(dir): - if not name.endswith(".py") and not name.endswith(".pyc"): - continue - if name.startswith("_") or name.startswith("."): - continue - modname = sub("\.pyc?$", "", name) # Remove extension - if modname not in processed: - self._load_task(modname, dir) - processed.append(modname) - - def load(self): - """Load (or reload) all valid tasks into self._tasks.""" - with self._task_access_lock: - self._tasks.clear() - builtin_dir = path.dirname(__file__) - plugins_dir = path.join(self.bot.config.root_dir, "tasks") - self._load_directory(builtin_dir) # Built-in tasks - self._load_directory(plugins_dir) # Custom tasks, aka plugins - - msg = "Loaded {0} tasks: {1}" - tasks = ', '.join(self._tasks.keys()) - self.logger.info(msg.format(len(self._tasks), tasks)) - - def start(self, task_name, **kwargs): - """Start a given task in a new thread. kwargs are passed to task.run""" - msg = "Starting task '{0}' in a new thread" - self.logger.info(msg.format(task_name)) - - with self._task_access_lock: - try: - task = self._tasks[task_name] - except KeyError: - e = "Couldn't find task '{0}':" - self.logger.error(e.format(task_name)) - return - - task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs) - start_time = strftime("%b %d %H:%M:%S") - task_thread.name = "{0} ({1})".format(task_name, start_time) - task_thread.start() - - def schedule(self, now=None): - """Start all tasks that are supposed to be run at a given time.""" - if not now: - now = gmtime() - # Get list of tasks to run this turn: - tasks = self.bot.config.schedule(now.tm_min, now.tm_hour, now.tm_mday, - now.tm_mon, now.tm_wday) - - for task in tasks: - if isinstance(task, list): # They've specified kwargs, - self.start(task[0], **task[1]) # so pass those to start - else: # Otherwise, just pass task_name - self.start(task) - - def get(self, task_name): - """Return the class instance associated with a certain task name. - - Will raise KeyError if the task is not found. - """ - return self._tasks[task_name] diff --git a/earwigbot/wiki/__init__.py b/earwigbot/wiki/__init__.py index f2f0e89..558754b 100644 --- a/earwigbot/wiki/__init__.py +++ b/earwigbot/wiki/__init__.py @@ -29,10 +29,10 @@ written by Mr.Z-man, other than a similar purpose. We share no code. Import the toolset directly with `from earwigbot import wiki`. If using the built-in integration with the rest of the bot, Bot() objects contain a `wiki` -attribute, which is a SitesDBManager object tied to the sites.db file located -in the same directory as config.yml. That object has the principal methods -get_site, add_site, and remove_site that should handle all of your Site (and -thus, Page, Category, and User) needs. +attribute, which is a SitesDB object tied to the sites.db file located in the +same directory as config.yml. That object has the principal methods get_site, +add_site, and remove_site that should handle all of your Site (and thus, Page, +Category, and User) needs. """ import logging as _log diff --git a/earwigbot/wiki/sitesdb.py b/earwigbot/wiki/sitesdb.py index a5c9fe7..7ad8bf8 100644 --- a/earwigbot/wiki/sitesdb.py +++ b/earwigbot/wiki/sitesdb.py @@ -32,9 +32,9 @@ from earwigbot import __version__ from earwigbot.wiki.exceptions import SiteNotFoundError from earwigbot.wiki.site import Site -__all__ = ["SitesDBManager"] +__all__ = ["SitesDB"] -class SitesDBManager(object): +class SitesDB(object): """ EarwigBot's Wiki Toolset: Sites Database Manager @@ -49,8 +49,7 @@ class SitesDBManager(object): here are available as bot.wiki.get_site(), bot.wiki.add_site(), and bot.wiki.remove_site(), which use a sites.db file located in the same directory as our config.yml file. Lower-level access can be achieved - by importing the manager class - (`from earwigbot.wiki import SitesDBManager`). + by importing the manager class (`from earwigbot.wiki import SitesDB`). """ def __init__(self, config):