Commits

Jason R. Coombs  committed a6a9059 Draft

Refactored output handling so that all output is treated as iterables and errors are now trapped in iterables. Fixes #19.

  • Participants
  • Parent commits 05ca1bd

Comments (0)

Files changed (6)

File pmxbot/botbase.py

 # vim:ts=4:sw=4:noexpandtab
 
+from __future__ import absolute_import
+
 import sys
 import datetime
 import os
 import traceback
 import time
 import random
-import StringIO
 import collections
 import textwrap
+import functools
 
 import irc.bot
 
+import pmxbot.itertools
 from . import karma
 from . import quotes
 from .logging import init_logger
 		c.privmsg(channel, "You summoned me, master %s?" % nick)
 
 	def _handle_output(self, channel, output):
-		if not output:
-			return
-
-		if isinstance(output, basestring):
-			# turn the string into an iterable of lines
-			output = StringIO.StringIO(output)
-
 		for secret, item in NoLog.secret_items(output):
 			self.out(channel, item, not secret)
 
 		"""
 		Wrapper to run scheduled type tasks cleanly.
 		"""
-		try:
-			self._handle_output(channel, func(c, None, *args))
-		except:
+		def on_error(exception):
 			print datetime.datetime.now(), "Error in background runner for ", func
 			traceback.print_exc()
+		func = functools.partial(func, c, None, *args)
+		self._handle_output(channel, pmxbot.itertools.trap_exceptions(
+			pmxbot.itertools.generate_results(func),
+			on_error))
+
+	def _handle_exception(self, exception, **kwargs):
+		explitives = ['Yikes!', 'Zoiks!', 'Ouch!']
+		explitive = random.choice(explitives)
+		res = ["{explitive} An error occurred: {exception}"
+			.format(**vars())]
+		res.append('!{name} {doc}'.format(**vars()))
+		print datetime.datetime.now(), ("Error with command {type}"
+			.format(**vars()))
+		traceback.print_exc()
+		return res
 
 	def handle_action(self, c, e, channel, nick, msg):
 		"""Core message parser and dispatcher"""
 		lc_msg = msg.lower()
-		lc_cmd = msg.split(' ', 1)[0]
+		cmd, _, cmd_args = msg.partition(' ')
+
 		res = None
 		for typ, name, f, doc, channels, exclude, rate, priority in _handler_registry:
-			if typ in ('command', 'alias') and lc_cmd == '!%s' % name:
-				# grab everything after the command
-				msg = msg.partition(' ')[2].strip()
-				try:
-					res = f(c, e, channel, nick, msg)
-				except Exception as exc:
-					explitives = ['Yikes!', 'Zoiks!', 'Ouch!']
-					explitive = random.choice(explitives)
-					res = ["{explitive} An error occurred: {exc}".format(**vars())]
-					res.append('!{name} {doc}'.format(**vars()))
-					print datetime.datetime.now(), "Error with command %s" % name
-					traceback.print_exc()
+			exception_handler = functools.partial(
+				self._handle_command_exception,
+				type = typ,
+				name = name,
+				doc = doc,
+				)
+			if typ in ('command', 'alias') and cmd.lower() == '!%s' % name:
+				f = functools.partial(f, c, e, channel, nick, cmd_args)
+				res = pmxbot.itertools.trap_exceptions(
+					pmxbot.itertools.generate_results(f),
+					exception_handler
+				)
 				break
 			elif typ in ('contains', '#') and name in lc_msg:
+				f = functools.partial(f, c, e, channel, nick, msg)
 				if (not channels and not exclude) \
 				or channel in channels \
 				or (exclude and channel not in exclude) \
 				or (channels == "unlogged" and channel in self._nolog) \
 				or (exclude == "logged" and channel in self._nolog) \
 				or (exclude == "unlogged" and channel in self._channels and channel not in self._nolog):
-					if random.random() <= rate:
-						try:
-							res = f(c, e, channel, nick, msg)
-						except Exception, e:
-							print datetime.datetime.now(), "Error with contains  %s" % name
-							traceback.print_exc()
-						break
+					if random.random() > rate:
+						continue
+					res = pmxbot.itertools.trap_exceptions(
+						pmxbot.itertools.generate_results(f),
+						exception_handler
+					)
+					break
 		self._handle_output(channel, res)
 
 

File pmxbot/itertools.py

+from __future__ import unicode_literals
+
+import io
+
+def always_iterable(item):
+	r"""
+	Given an item from a pmxbot handler, always return an iterable.
+
+	If the item is None, return an empty iterable.
+	>>> list(always_iterable(None))
+	[]
+
+	If the item is a string, return an iterable of the lines in the string.
+	>>> list(always_iterable('foo'))
+	[u'foo']
+	>>> list(always_iterable('foo\nbar'))
+	[u'foo\n', u'bar']
+
+	>>> list(always_iterable([1,2,3]))
+	[1, 2, 3]
+	>>> always_iterable(xrange(10))
+	xrange(10)
+
+	And any other non-iterable objects are returned as single-tuples of that
+	item.
+	>>> list(always_iterable(object()))  # doctest: +ELLIPSIS
+	[<object object at ...>]
+	"""
+	if item is None:
+		item = ()
+
+	if isinstance(item, basestring):
+		item = io.StringIO(unicode(item))
+
+	if not hasattr(item, '__iter__'):
+		item = item,
+
+	return item
+
+def generate_results(self, function):
+	"""
+	Take a function, which may return an iterator or a static result
+	and convert it to a late-dispatched generator.
+	"""
+	for item in always_iterable(function()):
+		yield item
+
+def trap_exceptions(self, results, handler, exceptions=Exception):
+	"""
+	Iterate through the results, but if an exception occurs, stop
+	processing the results and instead replace
+	the results with the output from the exception handler.
+	"""
+	try:
+		for result in results:
+			yield result
+	except exceptions as exc:
+		for result in always_iterable(handler(exc)):
+			yield result

File pmxbot/karma.py

 # vim:ts=4:sw=4:noexpandtab
 
+from __future__ import print_function
+
 import itertools
 import re
 

File pmxbot/logging.py

+
+from __future__ import absolute_import
+
 import re
 import random
 import datetime

File pmxbot/saysomething.py

 # vim:ts=4:sw=4:noexpandtab
+
+from __future__ import absolute_import
+
 import random
 from itertools import chain
 

File pmxbot/storage.py

-import os
+from __future__ import absolute_import
+
 import itertools
 import urlparse
 
 
 class SQLiteStorage(Storage):
 	scheme = 'sqlite'
-	
+
 	@classmethod
 	def uri_matches(cls, uri):
 		return uri.endswith('.sqlite')
 
 class MongoDBStorage(Storage):
 	scheme = 'mongodb'
-	
+
 	@classmethod
 	def uri_matches(cls, uri):
 		return uri.startswith('mongodb:')