From 60be7e7e7a4c4341342ee8c099d170cdeb982215 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 09:29:37 +0000 Subject: [PATCH 01/16] First layer for signals --- pyaib/signals.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pyaib/signals.py diff --git a/pyaib/signals.py b/pyaib/signals.py new file mode 100644 index 0000000..cc8ba1b --- /dev/null +++ b/pyaib/signals.py @@ -0,0 +1,20 @@ +def emit_signal(signal_name): + """Emits the signal of the given name.""" + +def await_signal(signal_name): + """Blocks until the signal of the given name is recieved.""" + +def awaits_signal(signal_name): + """Decorator; call this function when the signal is recieved.""" + def wrapper(func): + pass + return wrapper + +class Signal: + def __init__(self): + pass + +class Signals: + # Stores all the different signals. + # There are no pre-defined signals - they will be created by the end user. + pass From d3953ab55861036ef943327cee41714cd84c0a27 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 15:35:14 +0000 Subject: [PATCH 02/16] Declare signals in manifest --- pyaib/ircbot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyaib/ircbot.py b/pyaib/ircbot.py index 6bac5f8..4a6570f 100644 --- a/pyaib/ircbot.py +++ b/pyaib/ircbot.py @@ -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 @@ -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 From 839eaf58d4cd321f0dc30b16b69e527a0f3b350e Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 16:16:57 +0000 Subject: [PATCH 03/16] Begin implementing gevent events --- pyaib/signals.py | 105 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index cc8ba1b..bcfcd9a 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -1,20 +1,117 @@ -def emit_signal(signal_name): +#!/usr/bin/env python + +import collections +import gevent.event + +from . import irc + +def emit_signal(irc_c, name): """Emits the signal of the given name.""" + # TODO create signal if it doesn't already exist -def await_signal(signal_name): +def await_signal(irc_c, name, timeout=None): """Blocks until the signal of the given name is recieved.""" + # TODO create signal if it doesn't already exist + # add myself to the list of observers + event = irc_c.signals(name)._event + recieved = event.wait(timeout) + if not recieved: + raise TimeoutError("Waiting for signal %s timed out" % name) + return recieved + # remove myself from the list of observers -def awaits_signal(signal_name): +def awaits_signal(irc_c, name): """Decorator; call this function when the signal is recieved.""" + # XXX this shouldn't have irc_c? How do other decorators work + # TODO create signal if it doesn't already exist + # add this function to the list of observers def wrapper(func): pass return wrapper +def _unfire_signal(name, irc_c): + """Resets emitted signals for later reuse.""" + irc_c.signals[name].unfire() + class Signal: def __init__(self): - pass + self.__observers = [] # list of stuff waiting on this event + self._event = gevent.event.Event() + + def observe(self, observer): + # XXX this is ok for decorators but not normal waiters + if isinstance(observer, collections.Callable): + self.__observers.append(observer) + else: + # XXX should throw? + raise TypeError("%s not callable" % repr(observer)) + return self + + def unobserve(self, observer): + self.__observers.remove(observer) + return self + + def fire(self, irc_c, *args, **keywargs): + assert isinstance(irc_c, irc.Context) + # initiate or resume each observer + for observer in self.__observers: + if isinstance(observer, collections.Callable): + irc_c.bot_greenlets.spawn(observer, *args, **keywargs) + elif False: + # TODO resume existing wating greenlets + pass + else: + raise TypeError("%s not callable" % repr(observer)) + + def unfire(self): + # reset the gevent event + self._event.clear() + + def getObserverCount(self): + return len(self.__observers) + + def observers(self): + return self.__observers + + def __bool__(self): + return self.getObserverCount() > 0 + + __nonzero__ = __bool__ # 2.x compat + __iadd__ = observe + __isub__ = unobserve + __call__ = fire + __len__ = getObserverCount 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() + 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 From 306e0699accf7a237605237be97a9a1f9fa9553d Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 18:04:52 +0000 Subject: [PATCH 04/16] Move awaits_signals decorator to components (where watches is) --- pyaib/components.py | 8 ++++++++ pyaib/signals.py | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pyaib/components.py b/pyaib/components.py index b86fbb0..55486ae 100644 --- a/pyaib/components.py +++ b/pyaib/components.py @@ -108,6 +108,14 @@ def wrapper(func): handle = watches 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""" diff --git a/pyaib/signals.py b/pyaib/signals.py index bcfcd9a..a4a6cba 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -7,26 +7,29 @@ def emit_signal(irc_c, name): """Emits the signal of the given name.""" - # TODO create signal if it doesn't already exist + # create signal if it doesn't already exist + signal = irc_c.signals(name) + signal.fire(irc_c) def await_signal(irc_c, name, timeout=None): """Blocks until the signal of the given name is recieved.""" - # TODO create signal if it doesn't already exist - # add myself to the list of observers + # create signal if it doesn't already exist event = irc_c.signals(name)._event recieved = event.wait(timeout) if not recieved: raise TimeoutError("Waiting for signal %s timed out" % name) return recieved - # remove myself from the list of observers -def awaits_signal(irc_c, name): +def awaits_signal(name): + # XXX moved to components (where watches is) """Decorator; call this function when the signal is recieved.""" # XXX this shouldn't have irc_c? How do other decorators work - # TODO create signal if it doesn't already exist + # create signal if it doesn't already exist + signal = irc_c.signals(name) # add this function to the list of observers def wrapper(func): - pass + signal.observe(func) + return func return wrapper def _unfire_signal(name, irc_c): @@ -36,14 +39,13 @@ def _unfire_signal(name, irc_c): class Signal: def __init__(self): self.__observers = [] # list of stuff waiting on this event + self.__observers.append(_unfire_signal) self._event = gevent.event.Event() def observe(self, observer): - # XXX this is ok for decorators but not normal waiters if isinstance(observer, collections.Callable): self.__observers.append(observer) else: - # XXX should throw? raise TypeError("%s not callable" % repr(observer)) return self @@ -52,14 +54,12 @@ def unobserve(self, observer): return self def fire(self, irc_c, *args, **keywargs): + # initiate a decorated waiter. + # existing waiting greenlets are not affected by this assert isinstance(irc_c, irc.Context) - # initiate or resume each observer for observer in self.__observers: if isinstance(observer, collections.Callable): irc_c.bot_greenlets.spawn(observer, *args, **keywargs) - elif False: - # TODO resume existing wating greenlets - pass else: raise TypeError("%s not callable" % repr(observer)) From a60b1a09f9ff9011d79e8015b752e5d35a3666f0 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 18:31:52 +0000 Subject: [PATCH 05/16] Add signals to component manager --- pyaib/components.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyaib/components.py b/pyaib/components.py index 55486ae..966318b 100644 --- a/pyaib/components.py +++ b/pyaib/components.py @@ -108,6 +108,7 @@ def wrapper(func): handle = watches handles = watches + def awaits_signal(*signals): """Define a series of signals to later be subscribed to""" def wrapper(func): @@ -343,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(word).observe(method) def _add_parsers(self, method, name, chain): """ Handle Message parser adding and chaining """ From f3bd09fad1c9e05ea9397ea9686b2150e4251a64 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 18:32:31 +0000 Subject: [PATCH 06/16] Remove old decorator from signals --- pyaib/signals.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index a4a6cba..3fe35d7 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -20,18 +20,6 @@ def await_signal(irc_c, name, timeout=None): raise TimeoutError("Waiting for signal %s timed out" % name) return recieved -def awaits_signal(name): - # XXX moved to components (where watches is) - """Decorator; call this function when the signal is recieved.""" - # XXX this shouldn't have irc_c? How do other decorators work - # create signal if it doesn't already exist - signal = irc_c.signals(name) - # add this function to the list of observers - def wrapper(func): - signal.observe(func) - return func - return wrapper - def _unfire_signal(name, irc_c): """Resets emitted signals for later reuse.""" irc_c.signals[name].unfire() From 9800dbe75818e4386272c10d5e24524c95f125cc Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 19:45:36 +0000 Subject: [PATCH 07/16] Fix word->signal --- pyaib/components.py | 2 +- pyaib/signals.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyaib/components.py b/pyaib/components.py index 966318b..1b73d97 100644 --- a/pyaib/components.py +++ b/pyaib/components.py @@ -346,7 +346,7 @@ def _install_hooks(self, context, hooked_methods): self._add_parsers(method, name, chain) elif kind == 'signals': for signal in args: - context.signals(word).observe(method) + context.signals(signal).observe(method) def _add_parsers(self, method, name, chain): """ Handle Message parser adding and chaining """ diff --git a/pyaib/signals.py b/pyaib/signals.py index 3fe35d7..0b578d5 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -5,11 +5,11 @@ from . import irc -def emit_signal(irc_c, name): +def emit_signal(irc_c, name, data=None): """Emits the signal of the given name.""" # create signal if it doesn't already exist signal = irc_c.signals(name) - signal.fire(irc_c) + signal.fire(irc_c, data) def await_signal(irc_c, name, timeout=None): """Blocks until the signal of the given name is recieved.""" @@ -41,13 +41,16 @@ def unobserve(self, observer): self.__observers.remove(observer) return self - def fire(self, irc_c, *args, **keywargs): + def fire(self, *args, **kwargs): + # args kept in 1 argument to be passed to greenlet easily + irc_c = args[0] + data = args[1] # initiate a decorated waiter. # existing waiting greenlets are not affected by this assert isinstance(irc_c, irc.Context) for observer in self.__observers: if isinstance(observer, collections.Callable): - irc_c.bot_greenlets.spawn(observer, *args, **keywargs) + irc_c.bot_greenlets.spawn(observer, *args, **kwargs) else: raise TypeError("%s not callable" % repr(observer)) From 042798add33878b1721a6b248bd86f32162ee027 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 23:20:26 +0000 Subject: [PATCH 08/16] Add irc_c type check --- pyaib/signals.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index 0b578d5..c461316 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -5,14 +5,18 @@ from . import irc -def emit_signal(irc_c, name, data=None): +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") # 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): +def await_signal(irc_c, name, *, timeout=None): """Blocks until the signal of the given name is recieved.""" + if not isinstance(irc_c, irc.Context): + raise TypeError("First argument must be IRC context") # create signal if it doesn't already exist event = irc_c.signals(name)._event recieved = event.wait(timeout) @@ -22,6 +26,7 @@ def await_signal(irc_c, name, timeout=None): def _unfire_signal(name, irc_c): """Resets emitted signals for later reuse.""" + # TODO make arguments match what emission actually does irc_c.signals[name].unfire() class Signal: From 818fe6499186ed29a42c9d5dab1cdf407b9bc58d Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Sun, 26 Jan 2020 23:41:42 +0000 Subject: [PATCH 09/16] Fire the gevent event --- pyaib/signals.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index c461316..59976a8 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -20,7 +20,7 @@ def await_signal(irc_c, name, *, timeout=None): # create signal if it doesn't already exist event = irc_c.signals(name)._event recieved = event.wait(timeout) - if not recieved: + if recieved is False: raise TimeoutError("Waiting for signal %s timed out" % name) return recieved @@ -49,10 +49,10 @@ def unobserve(self, observer): def fire(self, *args, **kwargs): # args kept in 1 argument to be passed to greenlet easily irc_c = args[0] - data = args[1] - # initiate a decorated waiter. - # existing waiting greenlets are not affected by this assert isinstance(irc_c, irc.Context) + # activate the event for waiting existing greenlets + self._event.set() + # manually initiate decorated observers for observer in self.__observers: if isinstance(observer, collections.Callable): irc_c.bot_greenlets.spawn(observer, *args, **kwargs) From 322e18d2206896bb3684064137657d7972994496 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Mon, 27 Jan 2020 04:23:44 +0000 Subject: [PATCH 10/16] Add an example usage --- example/plugins/signals.py | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 example/plugins/signals.py diff --git a/example/plugins/signals.py b/example/plugins/signals.py new file mode 100644 index 0000000..95f1cb4 --- /dev/null +++ b/example/plugins/signals.py @@ -0,0 +1,50 @@ +"""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)) + + @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.pop(0) + names = response[:] + # 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. From f31cf4ead4d154314559fa35e830956234ed94a2 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Mon, 27 Jan 2020 05:43:29 +0000 Subject: [PATCH 11/16] Make event closure check happen after both conditions --- example/plugins/signals.py | 6 +++--- pyaib/signals.py | 41 ++++++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/example/plugins/signals.py b/example/plugins/signals.py index 95f1cb4..893b3ac 100644 --- a/example/plugins/signals.py +++ b/example/plugins/signals.py @@ -33,7 +33,7 @@ def get_list_of_names(self, irc_c, message, trigger, args, kwargs): channel = response[0] names = response[1] assert channel == message.channel - message.reply("List of channel members: %s" % ", ".join(names)) + message.reply("List of channel members: %s" % len(names)) @observe('IRC_MSG_353') # 353 indicates a NAMES response. def recieve_names(self, irc_c, message): @@ -41,8 +41,8 @@ def recieve_names(self, irc_c, message): # "MYNICK = #channel :nick1 nick2 nick3" # Split that up into individual names: response = re.split(r"\s:?", message.args.strip())[2:] - channel = response.pop(0) - names = response[:] + 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)) diff --git a/pyaib/signals.py b/pyaib/signals.py index 59976a8..ce98fcf 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -2,11 +2,13 @@ import collections import gevent.event +from copy import copy from . import irc def emit_signal(irc_c, name, *, data=None): """Emits the signal of the given name.""" + print('Emitting {} with {}'.format(name, data)) if not isinstance(irc_c, irc.Context): raise TypeError("First argument must be IRC context") # create signal if it doesn't already exist @@ -18,22 +20,20 @@ def await_signal(irc_c, name, *, timeout=None): if not isinstance(irc_c, irc.Context): raise TypeError("First argument must be IRC context") # create signal if it doesn't already exist - event = irc_c.signals(name)._event - recieved = event.wait(timeout) + signal = irc_c.signals(name) + recieved = signal._event.wait(timeout) if recieved is False: raise TimeoutError("Waiting for signal %s timed out" % name) - return recieved - -def _unfire_signal(name, irc_c): - """Resets emitted signals for later reuse.""" - # TODO make arguments match what emission actually does - irc_c.signals[name].unfire() + data = copy(signal._data) + print('Found {} with {}'.format(name, data)) + return data class Signal: - def __init__(self): + def __init__(self, name): self.__observers = [] # list of stuff waiting on this event - self.__observers.append(_unfire_signal) self._event = gevent.event.Event() + self._data = None + self.name = name def observe(self, observer): if isinstance(observer, collections.Callable): @@ -46,22 +46,33 @@ def unobserve(self, observer): self.__observers.remove(observer) return self - def fire(self, *args, **kwargs): - # args kept in 1 argument to be passed to greenlet easily - irc_c = args[0] + def fire(self, irc_c, data): assert isinstance(irc_c, irc.Context) + # Queue the function that unfires this event # activate the event for waiting existing greenlets + self._data = data self._event.set() # manually initiate decorated observers for observer in self.__observers: if isinstance(observer, collections.Callable): - irc_c.bot_greenlets.spawn(observer, *args, **kwargs) + irc_c.bot_greenlets.spawn(observer, irc_c, copy(data)) else: raise TypeError("%s not callable" % repr(observer)) + # finally, initiate the unfiring event + irc_c.bot_greenlets.spawn(self.wait_then_unfire, irc_c) def unfire(self): # reset the gevent event self._event.clear() + self._data = None + + def wait_then_unfire(self, irc_c): + print("UNF Waiting to unfire {}".format(self.name)) + # Waits for the signal, then unfires it. + # Guaranteed to be the last existing waiter executed. + await_signal(irc_c, self.name) + print("UNF Unfiring {}".format(self.name)) + self.unfire() def getObserverCount(self): return len(self.__observers) @@ -94,7 +105,7 @@ def isSignal(self, name): def getOrMake(self, name): if not self.isSignal(name): #Make Event if it does not exist - self.__signals[name.lower()] = Signal() + self.__signals[name.lower()] = Signal(name) return self.get(name) #Return the null signal on non existent signal From e57a9bf4aba6b7c9a594c075f300a1bea2963978 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Mon, 27 Jan 2020 05:56:18 +0000 Subject: [PATCH 12/16] Update example --- example/plugins/signals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/plugins/signals.py b/example/plugins/signals.py index 893b3ac..e08a5ee 100644 --- a/example/plugins/signals.py +++ b/example/plugins/signals.py @@ -33,7 +33,8 @@ def get_list_of_names(self, irc_c, message, trigger, args, kwargs): channel = response[0] names = response[1] assert channel == message.channel - message.reply("List of channel members: %s" % len(names)) + 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): From eb14edaebb2107eec20125a70cf2e5b6600a195f Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Mon, 27 Jan 2020 05:56:48 +0000 Subject: [PATCH 13/16] Remove debug statements --- pyaib/signals.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index ce98fcf..af72bc2 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -8,7 +8,6 @@ def emit_signal(irc_c, name, *, data=None): """Emits the signal of the given name.""" - print('Emitting {} with {}'.format(name, data)) if not isinstance(irc_c, irc.Context): raise TypeError("First argument must be IRC context") # create signal if it doesn't already exist @@ -25,7 +24,6 @@ def await_signal(irc_c, name, *, timeout=None): if recieved is False: raise TimeoutError("Waiting for signal %s timed out" % name) data = copy(signal._data) - print('Found {} with {}'.format(name, data)) return data class Signal: From 17e5fc363d0f61ce11a219d10864e1a8db0e5c15 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Mon, 27 Jan 2020 06:49:35 +0000 Subject: [PATCH 14/16] Optimise closure check --- pyaib/signals.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index af72bc2..3d384ad 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -57,19 +57,17 @@ def fire(self, irc_c, data): else: raise TypeError("%s not callable" % repr(observer)) # finally, initiate the unfiring event - irc_c.bot_greenlets.spawn(self.wait_then_unfire, irc_c) + irc_c.bot_greenlets.spawn(self.wait_then_unfire) def unfire(self): # reset the gevent event self._event.clear() self._data = None - def wait_then_unfire(self, irc_c): - print("UNF Waiting to unfire {}".format(self.name)) + def wait_then_unfire(self): # Waits for the signal, then unfires it. # Guaranteed to be the last existing waiter executed. - await_signal(irc_c, self.name) - print("UNF Unfiring {}".format(self.name)) + self._event.wait() self.unfire() def getObserverCount(self): From 095c9b603ea6feff330009ef917107d003b28e09 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Mon, 27 Jan 2020 11:05:42 +0000 Subject: [PATCH 15/16] Remove automatic unfire; add clear_signal --- pyaib/signals.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index 3d384ad..1c7ebe1 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -26,6 +26,15 @@ def await_signal(irc_c, name, *, timeout=None): data = copy(signal._data) return data +def clear_signal(irc_c, name): + """Stop emitting the signal of the given name.""" + if not isinstance(irc_c, irc.Context): + raise TypeError("First argument must be IRC context") + if not name in irc_c.signals.list(): + raise ValueError("Signal %s doesn't exist" % name) + signal = irc_c.signals[name] + signal.unfire() + class Signal: def __init__(self, name): self.__observers = [] # list of stuff waiting on this event @@ -46,7 +55,6 @@ def unobserve(self, observer): def fire(self, irc_c, data): assert isinstance(irc_c, irc.Context) - # Queue the function that unfires this event # activate the event for waiting existing greenlets self._data = data self._event.set() @@ -56,34 +64,28 @@ def fire(self, irc_c, data): irc_c.bot_greenlets.spawn(observer, irc_c, copy(data)) else: raise TypeError("%s not callable" % repr(observer)) - # finally, initiate the unfiring event - irc_c.bot_greenlets.spawn(self.wait_then_unfire) + # signal now needs to be unfired by the user def unfire(self): # reset the gevent event self._event.clear() self._data = None - def wait_then_unfire(self): - # Waits for the signal, then unfires it. - # Guaranteed to be the last existing waiter executed. - self._event.wait() - self.unfire() - - def getObserverCount(self): - return len(self.__observers) + # Observer counts are inaccurate: no way to tell how many existing waiters + # def getObserverCount(self): + # return len(self.__observers) - def observers(self): - return self.__observers + # def observers(self): + # return self.__observers - def __bool__(self): - return self.getObserverCount() > 0 + # def __bool__(self): + # return self.getObserverCount() > 0 - __nonzero__ = __bool__ # 2.x compat + # __nonzero__ = __bool__ # 2.x compat __iadd__ = observe __isub__ = unobserve - __call__ = fire - __len__ = getObserverCount + # __call__ = fire + # __len__ = getObserverCount class Signals: # Stores all the different signals. From 70ff970698c866ca4972947cd2cc6d19839c8cb2 Mon Sep 17 00:00:00 2001 From: Ross Williams Date: Tue, 28 Jan 2020 03:40:15 +0000 Subject: [PATCH 16/16] Implement jamadden solution --- pyaib/signals.py | 69 ++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/pyaib/signals.py b/pyaib/signals.py index 1c7ebe1..234f655 100644 --- a/pyaib/signals.py +++ b/pyaib/signals.py @@ -2,7 +2,8 @@ import collections import gevent.event -from copy import copy +import gevent.queue +import gevent from . import irc @@ -10,36 +11,26 @@ 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.""" + """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) - recieved = signal._event.wait(timeout) - if recieved is False: - raise TimeoutError("Waiting for signal %s timed out" % name) - data = copy(signal._data) - return data - -def clear_signal(irc_c, name): - """Stop emitting the signal of the given name.""" - if not isinstance(irc_c, irc.Context): - raise TypeError("First argument must be IRC context") - if not name in irc_c.signals.list(): - raise ValueError("Signal %s doesn't exist" % name) - signal = irc_c.signals[name] - signal.unfire() + return signal.wait(timeout) class Signal: def __init__(self, name): - self.__observers = [] # list of stuff waiting on this event - self._event = gevent.event.Event() - self._data = None + self.__event = gevent.event.Event() + self.__observers = [] # decorated observers + self.__waiters = [] # waiting greenlets self.name = name def observe(self, observer): @@ -55,37 +46,29 @@ def unobserve(self, observer): def fire(self, irc_c, data): assert isinstance(irc_c, irc.Context) - # activate the event for waiting existing greenlets - self._data = data - self._event.set() + # 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)) - # signal now needs to be unfired by the user - - def unfire(self): - # reset the gevent event - self._event.clear() - self._data = None - - # Observer counts are inaccurate: no way to tell how many existing waiters - # def getObserverCount(self): - # return len(self.__observers) - - # def observers(self): - # return self.__observers - - # def __bool__(self): - # return self.getObserverCount() > 0 - # __nonzero__ = __bool__ # 2.x compat - __iadd__ = observe - __isub__ = unobserve - # __call__ = fire - # __len__ = getObserverCount + @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.