Skip to content

Instantly share code, notes, and snippets.

@danbradham
Last active August 29, 2015 14:04
Show Gist options
  • Save danbradham/43254d05a7fb87e31c4a to your computer and use it in GitHub Desktop.
Save danbradham/43254d05a7fb87e31c4a to your computer and use it in GitHub Desktop.
Shout!

While this started as a quick gist, I ended up developing it far enough to give it it's own repository and documentation.

Visit Shout's Repo!

'''
Shout
-----
A loud messaging module.
'''
import inspect
from collections import Sequence, defaultdict
import sys
ROOM_DEFL = "void"
class MetaMsg(type):
'''Metaclass adding a listeners dict allowing subclasses to keep
track of listeners and their methods.'''
def __new__(kls, name, bases, members):
cls = super(MetaMsg, kls).__new__(kls, name, bases, members)
cls.listeners = defaultdict(set)
return cls
class Message(object):
__metaclass__ = MetaMsg
def __init__(self, *args, **kwargs):
try:
self.room = kwargs.pop("to")
except KeyError:
self.room = ROOM_DEFL
self.args = args
self.kwargs = kwargs
self.results = []
self.exc = None
self.success = False
def shout(self):
listeners = self.listeners[self.room]
if not listeners:
self.exc = UserWarning(
"Nobody is listening to room: {0}".format(self.room))
return
for listener in listeners:
try:
result = listener(*self.args, **self.kwargs)
self.results.append(result)
except:
self.exc = sys.exc_info()
return
self.success = True
@classmethod
def add_listener(cls, fn):
for room in fn.rooms:
cls.listeners[room].add(fn)
return cls
@classmethod
def rem_listener(cls, fn):
for room_set in cls.listeners.itervalues():
cls.listeners.discard(fn)
return cls
@staticmethod
def new(name):
message = type(name, (Message,), {})
return message
class HasEars(object):
def __init__(self, *args, **kwargs):
members = inspect.getmembers(self.__class__)
for name, member in members:
if getattr(member, "has_ears", False):
method = getattr(self, member.__name__)
for msg_type in member.msg_types:
msg_type.add_listener(method)
super(HasEars, self).__init__(*args, **kwargs)
def typecheck_args(args):
'''Ensures all objects in sequence are of type Message.'''
if isinstance(args, Sequence):
for item in args:
if not issubclass(item, Message):
raise TypeError(
"All arguments passed to hears must be"
" subclasses of Message")
return True
raise TypeError(
"Wrong argument signature passed to hears decorator..."
"Pass a Message subclass or multiple Message subclasses.")
def hears(*args, **kwargs):
'''Wrap a function or Node method to hear Messages. Pass Node names to the
rooms keyword to limit the method to hear only Messages from certain
Node objects.'''
def wrapper(fn):
typecheck_args(args) # Make sure all our args are Message Subclasses
fn.has_ears = True
fn.msg_types = args
fn.rooms = kwargs.get("inside", (ROOM_DEFL,))
if isinstance(fn.rooms, basestring):
fn.rooms = (fn.rooms,)
argspec = inspect.getargspec(fn)
if argspec.args and argspec.args[0] == "self":
return fn
for msg_type in fn.msg_types:
msg_type.add_listener(fn)
return wrapper
def shout(msg_type, *args, **kwargs):
'''A convenience method for shouting Message instances.'''
msg = msg_type(*args, **kwargs)
msg.shout()
return msg
from shout import Message, HasEars, hears, shout, typecheck_args
from nose.tools import *
class GetClasses(Message):
pass
class GetException(Message):
pass
class Greet(Message):
pass
class SendArgsKwargs(Message):
pass
class A(HasEars):
@hears(Greet)
def a_class_method(self):
return "Hi from a!"
@hears(GetClasses)
def give_class(self):
return self.__class__
class B(HasEars):
@hears(Greet, inside=("B", "C"))
def b_class_method(self):
return "Hi from b!"
@hears(GetClasses)
def give_class(self):
return self.__class__
@hears(Greet, inside=("Unbound",))
def module_level_fn():
return "Hi from module_level_fn!"
@hears(SendArgsKwargs)
def module_level_fn_args_kwargs(*args, **kwargs):
return args, kwargs
@hears(GetException)
def module_level_fn_raises_exc(*args, **kwargs):
raise AttributeError("Bad error!")
class Test_Shout(object):
@classmethod
def setup_class(cls):
a = A()
b = B()
def test_shout(self):
msg = shout(Greet)
assert msg.results == ['Hi from a!']
def test_room(self):
msg = shout(Greet, to="Unbound")
assert msg.results == ["Hi from module_level_fn!"]
def test_rooms(self):
msg_b = shout(Greet, to="B")
msg_c = shout(Greet, to="C")
assert msg_b.results == ["Hi from b!"]
assert msg_c.results == ["Hi from b!"]
def test_exc(self):
msg_e = shout(GetException)
assert isinstance(msg_e.exc[1], AttributeError)
def test_results(self):
msg = shout(GetClasses)
assert A in msg.results
assert B in msg.results
def test_args_kwargs(self):
msg = shout(SendArgsKwargs, "oh yes", kwarg="right")
assert (("oh yes", ), {"kwarg": "right"}) in msg.results
@raises(TypeError)
def test_typecheck_args(self):
args = (Greet, GetClasses, SendArgsKwargs)
assert typecheck_args(args)
args = ("A", Greet)
typecheck_args(args)
args = "A"
typecheck_args(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment