From 99d0e7588f3cffa1efe3fb809490e9831f67b8e2 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 7 Aug 2011 22:44:20 -0400 Subject: [PATCH] loading commands now works, along with 'help' and 'test'; docstring fixes, etc --- bot/classes/base_command.py | 13 +++++-- bot/classes/base_task.py | 2 +- bot/commands/__init__.py | 82 ++++++++++++++++++++++++++------------------- bot/commands/help.py | 13 ++++--- bot/tasks/__init__.py | 2 +- 5 files changed, 66 insertions(+), 46 deletions(-) diff --git a/bot/classes/base_command.py b/bot/classes/base_command.py index 710e6eb..f323345 100644 --- a/bot/classes/base_command.py +++ b/bot/classes/base_command.py @@ -14,6 +14,13 @@ class BaseCommand(object): hooks = ["msg"] def __init__(self, connection): + """Constructor for new commands. + + This is called once when the command is loaded (from + commands._load_command()). `connection` is a Connection object, + allowing us to do self.connection.say(), self.connection.send(), etc, + from within a method. + """ self.connection = connection def check(self, data): @@ -34,8 +41,8 @@ class BaseCommand(object): """Main entry point for doing a command. Handle an activity (usually a message) on IRC. At this point, thanks - to self.check() which is called automatically by command_handler, we - know this is something we should respond to, so (usually) a - 'if data.command != "command_name": return' is unnecessary. + to self.check() which is called automatically by the command handler, + we know this is something we should respond to, so (usually) something + like 'if data.command != "command_name": return' is unnecessary. """ pass diff --git a/bot/classes/base_task.py b/bot/classes/base_task.py index e470178..278a77d 100644 --- a/bot/classes/base_task.py +++ b/bot/classes/base_task.py @@ -8,7 +8,7 @@ class BaseTask(object): """Constructor for new tasks. This is called once immediately after the task class is loaded by - the task manager (in tasks._load_class_from_file()). + the task manager (in tasks._load_task()). """ pass diff --git a/bot/commands/__init__.py b/bot/commands/__init__.py index c479490..7cb27be 100644 --- a/bot/commands/__init__.py +++ b/bot/commands/__init__.py @@ -8,53 +8,67 @@ In __init__, you can find some functions used to load and run these commands. """ import os +import sys import traceback +from classes import BaseCommand +import config + __all__ = ["load", "get_all", "check"] +# Base directory when searching for commands: +base_dir = os.path.join(config.root_dir, "bot", "commands") + # Store commands in a dict, where the key is the command's name and the value # is an instance of the command's class: _commands = {} -def _load_class_from_file(connection, module): - """Add.""" - global commands - objects = dir(module) +def _load_command(connection, filename): + """Try to load a specific command from a module, identified by file name. - for this_obj in objects: # go through everything in the file - obj = eval("module.%s" % this_obj) # this_obj is a string, so get the actual object corresponding to that string + Given a Connection object and a filename, we'll first try to import it, + and if that works, make an instance of the 'Command' class inside (assuming + it is an instance of BaseCommand), add it to _commands, and report the + addition to the user. Any problems along the way will either be ignored or + reported. + """ + global _commands - try: - bases = obj.__bases__ - except AttributeError: # object isn't a valid class, so ignore it - continue + # Strip .py from the end of the filename and join with our package name: + name = ".".join(("commands", filename[:-3])) + try: + __import__(name) + except: + print "Couldn't load file {0}:".format(filename) + traceback.print_exc() + return - for base in bases: - if base.__name__ == "BaseCommand": # this inherits BaseCommand, so it must be a command class - command = obj(connection) # initialize a new command object - _commands.append(command) - print "Added command class %s from %s..." % (this_obj, module.__name__) - continue + command = sys.modules[name].Command(connection) + if not isinstance(command, BaseCommand): + return + + _commands[command.name] = command + print "Added command {0}...".format(command.name) def load(connection): - """Load all valid commands into the _commands global variable.""" - files = os.listdir(os.path.join("bot", "commands")) + """Load all valid commands into the _commands global variable. + + `connection` is a Connection object that is given to each command's + constructor. + """ + files = os.listdir(base_dir) files.sort() - for f in files: - if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" + for filename in files: + if filename.startswith("_") or not filename.endswith(".py"): continue - module = f[:-3] # strip .py from end try: - exec "from irc.commands import %s" % module - except: # importing the file failed for some reason... - print "Couldn't load file %s:" % f - traceback.print_exc() - continue - process_module(connection, eval(module)) # 'module' is a string, so get the actual object for processing by eval-ing it + _load_command(connection, filename) + except AttributeError: + pass # The file is doesn't contain a command, so just move on - pretty_cmnds = map(lambda c: c.__class__.__name__, commands) - print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) + msg = "Found {0} command classes: {1}." + print msg.format(len(_commands), ", ".join(_commands.keys())) def get_all(): """Return our dict of all loaded commands.""" @@ -62,15 +76,15 @@ def get_all(): def check(hook, data): """Given an event on IRC, check if there's anything we can respond to.""" - # parse command arguments into data.command and data.args + # Parse command arguments into data.command and data.args: data.parse_args() - for command in _commands: - if hook in command.get_hooks(): + for command in _commands.values(): + if hook in command.hooks: if command.check(data): try: command.process(data) except: - print "Error executing command '{}':".format(data.command) - traceback.print_exc() # catch exceptions and print them + print "Error executing command '{0}':".format(data.command) + traceback.print_exc() break diff --git a/bot/commands/help.py b/bot/commands/help.py index dc98f16..6f57381 100644 --- a/bot/commands/help.py +++ b/bot/commands/help.py @@ -4,11 +4,11 @@ from classes import BaseCommand, Data import commands class Command(BaseCommand): - """Generates help information.""" + """Displays help information.""" name = "help" def process(self, data): - self.cmnds = commands.get_all().keys() + self.cmnds = commands.get_all() if not data.args: self.do_main_help(data) else: @@ -17,7 +17,7 @@ class Command(BaseCommand): def do_main_help(self, data): """Give the user a general help message with a list of all commands.""" msg = "I am a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help '." - msg.format(len(self.cmnds), ', '.join(self.cmnds)) + msg = msg.format(len(self.cmnds.keys()), ', '.join(self.cmnds)) self.connection.reply(data, msg) def do_command_help(self, data): @@ -26,17 +26,16 @@ class Command(BaseCommand): # Create a dummy message to test which commands pick up the user's # input: - dummy = Data() + dummy = Data(1) dummy.command = command.lower() dummy.is_command = True - for cmnd in self.cmnds: + for cmnd in self.cmnds.values(): if cmnd.check(dummy): doc = cmnd.__doc__ if doc: msg = "info for command \x0303{0}\x0301: \"{1}\"" - msg.format(command, doc) - self.connection.reply(data, msg) + self.connection.reply(data, msg.format(command, doc)) return break diff --git a/bot/tasks/__init__.py b/bot/tasks/__init__.py index 98ae925..9d03547 100644 --- a/bot/tasks/__init__.py +++ b/bot/tasks/__init__.py @@ -20,7 +20,7 @@ __all__ = ["load", "schedule", "start"] # an instance of the task class: _tasks = {} -def _load_class_from_file(f): +def _load_task(f): """Look in a given file for the task class.""" global _tasks