Skip to content
This repository has been archived by the owner on Aug 31, 2021. It is now read-only.

Passing data between greenlets #24

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
51 changes: 51 additions & 0 deletions example/plugins/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Example file for signals."""

from pyiab.plugins import plugin_class
from pyaib.components import observe, awaits_signal
from pyaib.signals import emit_signal, await_signal
import re

@plugin_class('names')
class Names:
"""This plugin provides a command ('names') that outputs a list of all
nicks currently in the channel."""
def __init__(self, irc_c, config):
print("Names plugin loaded")

@keyword('names')
def get_list_of_names(self, irc_c, message, trigger, args, kwargs):
# Sends a NAMES request to the server, to get a list of nicks for the
# current channel.
# Issue the NAMES request:
irc_c.RAW("NAMES %s" % message.channel)
# The request has been sent.
# pyaib is asynchronous, so another function will recieve the response
# from this request.
# That function must send the data here via a signal.
try:
# Wait for the signal (up to 10 seconds).
response = await_signal(irc_c, 'NAMES_RESPONSE', timeout=10.0)
# await_signal returns whatever data we choose to send, or True.
except TimeoutError:
message.reply("The request timed out.")
return
# The NAMES response is now saved.
channel = response[0]
names = response[1]
assert channel == message.channel
message.reply("List of channel members: %s" % ", ".join(names))
# Warning, this will annoy everyone in the channel.

@observe('IRC_MSG_353') # 353 indicates a NAMES response.
def recieve_names(self, irc_c, message):
# The response is in message.args as a single string.
# "MYNICK = #channel :nick1 nick2 nick3"
# Split that up into individual names:
response = re.split(r"\s:?", message.args.strip())[2:]
channel = response[0]
names = response[1:]
# Great, we've caught the NAMES response.
# Now send it back to the function that wanted it.
emit_signal(irc_c, 'NAMES_RESPONSE', data=(channel, names))
# The signal name can be anything, so long as emit_signal and
# await_signal use the same one.
12 changes: 12 additions & 0 deletions pyaib/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ def wrapper(func):
handles = watches


def awaits_signal(*signals):
"""Define a series of signals to later be subscribed to"""
def wrapper(func):
splugs = _get_plugs(func, 'signals')
splugs.extend([signal for signal in signals if signal not in splugs])
return func
return wrapper


class _Ignore(EasyDecorator):
"""Only pass if triggers is from user not ignored"""
def wrapper(dec, irc_c, msg, *args):
Expand Down Expand Up @@ -335,6 +344,9 @@ def _install_hooks(self, context, hooked_methods):
elif kind == 'parsers':
for name, chain in args:
self._add_parsers(method, name, chain)
elif kind == 'signals':
for signal in args:
context.signals(signal).observe(method)

def _add_parsers(self, method, name, chain):
""" Handle Message parser adding and chaining """
Expand Down
2 changes: 2 additions & 0 deletions pyaib/ircbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .config import Config
from .events import Events
from .timers import Timers
from .signals import Signals
from .components import ComponentManager
from . import irc

Expand All @@ -54,6 +55,7 @@ def __init__(self, *args, **kargs):
#Install most basic fundamental functionality
install('events', self._loadComponent(Events, False))
install('timers', self._loadComponent(Timers, False))
install('signals', self._loadComponent(Signals, False))

#Load the ComponentManager and load components
autoload = ['triggers', 'channels', 'plugins'] # Force these to load
Expand Down
105 changes: 105 additions & 0 deletions pyaib/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python

import collections
import gevent.event
import gevent.queue
import gevent

from . import irc

def emit_signal(irc_c, name, *, data=None):
"""Emits the signal of the given name."""
if not isinstance(irc_c, irc.Context):
raise TypeError("First argument must be IRC context")
if data is False:
raise ValueError("Signalled data cannot be False")
# create signal if it doesn't already exist
signal = irc_c.signals(name)
signal.fire(irc_c, data)

def await_signal(irc_c, name, *, timeout=None):
"""Blocks until the signal of the given name is recieved, returning any
data that was passed to it."""
if not isinstance(irc_c, irc.Context):
raise TypeError("First argument must be IRC context")
# create signal if it doesn't already exist
signal = irc_c.signals(name)
return signal.wait(timeout)

class Signal:
def __init__(self, name):
self.__event = gevent.event.Event()
self.__observers = [] # decorated observers
self.__waiters = [] # waiting greenlets
self.name = name

def observe(self, observer):
if isinstance(observer, collections.Callable):
self.__observers.append(observer)
else:
raise TypeError("%s not callable" % repr(observer))
return self

def unobserve(self, observer):
self.__observers.remove(observer)
return self

def fire(self, irc_c, data):
assert isinstance(irc_c, irc.Context)
# resume waiting greenlets
waiters = list(self.__waiters)
self.__waiters.clear()
gevent.spawn(self._notify, waiters, data)
# manually initiate decorated observers
for observer in self.__observers:
if isinstance(observer, collections.Callable):
irc_c.bot_greenlets.spawn(observer, irc_c, copy(data))
else:
raise TypeError("%s not callable" % repr(observer))

@staticmethod
def _notify(waiters, data):
for queue in waiters:
queue.put_nowait(data)

def wait(self, timeout):
queue = gevent.queue.Channel()
self.__waiters.append(queue)
data = queue.get(timeout)
if data is False:
raise TimeoutError("The request timed out.")
return data

class Signals:
# Stores all the different signals.
# There are no pre-defined signals - they will be created by the end user.
def __init__(self, irc_c):
self.__signals = {}
self.__nullSignal = NullSignal() # is this necessary?

def list(self):
return self.__signals.keys()

def isSignal(self, name):
return name.lower() in self.__signals

def getOrMake(self, name):
if not self.isSignal(name):
#Make Event if it does not exist
self.__signals[name.lower()] = Signal(name)
return self.get(name)

#Return the null signal on non existent signal
def get(self, name):
signal = self.__signals.get(name.lower())
if signal is None: # Only on undefined events
return self.__nullSignal
return signal

__contains__ = isSignal
__call__ = getOrMake
__getitem__ = get

class NullSignal:
# not sure this is even needed
pass