summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--ranger/ext/signals.py190
1 files changed, 175 insertions, 15 deletions
diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py
index 5d80590a..f9cdf9f0 100644
--- a/ranger/ext/signals.py
+++ b/ranger/ext/signals.py
@@ -13,36 +13,140 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+"""
+An efficient and minimalistic signaling/hook module.
+
+To use this in a class, subclass SignalDispatcher and call
+SignalDispatcher.__init__(self) in the __init__ function.  Now you can bind
+functions to a signal name (string) by using signal_bind or remove it with
+signal_unbind.  Now whenever signal_emit is called with that signal name,
+the bound functions are executed in order of priority.
+
+This module supports weak referencing.  This means that if you bind a function
+which is later deleted everywhere except in this binding, Python's garbage
+collector will remove it from memory.  Activate it with
+signal_bind(..., weak=True).  The handlers for such functions are automatically
+deleted when trying to call them (in signal_emit), but if they are never
+called, they accumulate and should be manually deleted with
+signal_garbage_collect().
+
+>>> def test_function(signal):
+... 	if 'display' in signal:
+... 		print(signal.display)
+... 	else:
+... 		signal.stop()
+>>> def temporary_function():
+... 	print("A temporary function")
+
+>>> sig = SignalDispatcher()
+
+>>> # Test binding and unbinding
+>>> handler1 = sig.signal_bind('test', test_function, priority=2)
+>>> handler2 = sig.signal_bind('test', temporary_function, priority=1)
+>>> sig.signal_emit('test', display="It works!")
+It works!
+A temporary function
+True
+>>> # Note that test_function stops the signal when there's no display keyword
+>>> sig.signal_emit('test')
+False
+>>> sig.signal_unbind(handler1)
+>>> sig.signal_emit('test')
+A temporary function
+True
+>>> sig.signal_clear()
+>>> sig.signal_emit('test')
+True
+
+>>> # Bind temporary_function with a weak reference
+>>> handler = sig.signal_bind('test', temporary_function, weak=True)
+>>> sig.signal_emit('test')
+A temporary function
+True
+>>> # Delete temporary_function.  Its handler is removed too, since it
+>>> # was weakly referenced.
+>>> del temporary_function
+>>> sig.signal_emit('test')
+True
+"""
+
 import weakref
 from types import MethodType
 
 class Signal(dict):
+	"""
+	Signals are passed to the bound functions as an argument.
+
+	They contain the attributes "origin", which is a reference to the
+	signal dispatcher, and "name", the name of the signal that was emitted.
+	You can call signal_emit with any keyword arguments, which will be
+	turned into attributes of this object as well.
+
+	To delete a signal handler from inside a signal, raise a ReferenceError.
+	"""
 	stopped = False
 	def __init__(self, **keywords):
 		dict.__init__(self, keywords)
 		self.__dict__ = self
 
 	def stop(self):
+		""" Stop the propagation of the signal to the next handlers.  """
 		self.stopped = True
 
 
-class SignalHandler(object):
+class SignalHandler:
+	"""
+	Signal Handlers contain information about a signal binding.
+
+	They are returned by signal_bind() and have to be passed to signal_unbind()
+	in order to remove the handler again.
+
+	You can disable a handler without removing it by setting the attribute
+	"active" to False.
+	"""
 	active = True
 	def __init__(self, signal_name, function, priority, pass_signal):
-		self.priority = max(0, min(1, priority))
-		self.signal_name = signal_name
-		self.function = function
-		self.pass_signal = pass_signal
+		self._priority = max(0, min(1, priority))
+		self._signal_name = signal_name
+		self._function = function
+		self._pass_signal = pass_signal
 
 
 class SignalDispatcher(object):
+	"""
+	This abstract class handles the binding and emitting of signals.
+	"""
 	def __init__(self):
 		self._signals = dict()
 
-	signal_clear = __init__
+	def signal_clear(self):
+		""" Remove all signals.  """
+		for handler_list in self._signals.values():
+			for handler in handler_list:
+				handler._function = None
+		self._signals = dict()
 
 	def signal_bind(self, signal_name, function, priority=0.5, weak=False):
+		"""
+		Bind a function to the signal.
+
+		signal_name:  Any string to name the signal
+		function:  Any function with either one or zero arguments which will be
+			called when the signal is emitted.  If it takes one argument, a
+			Signal object will be passed to it.
+		priority:  Optional, any number.  When signals are emitted, handlers will
+			be called in order of priority.  (highest priority first)
+		weak:  Use a weak reference of "function" so it can be garbage collected
+			properly when it's deleted.
+
+		Returns a SignalHandler which can be used to remove this binding by
+		passing it to signal_unbind().
+		"""
 		assert isinstance(signal_name, str)
+		assert hasattr(function, '__call__')
+		assert hasattr(function, '__code__')
+		assert isinstance(priority, (int, float))
+		assert isinstance(weak, bool)
 		try:
 			handlers = self._signals[signal_name]
 		except:
@@ -60,35 +164,85 @@ class SignalDispatcher(object):
 				function = (function.__func__, weakref.proxy(function.__self__))
 		handler = SignalHandler(signal_name, function, priority, nargs > 0)
 		handlers.append(handler)
-		handlers.sort(key=lambda handler: -handler.priority)
+		handlers.sort(key=lambda handler: -handler._priority)
 		return handler
 
 	def signal_unbind(self, signal_handler):
+		"""
+		Removes a signal binding.
+
+		This requires the SignalHandler that has been originally returned by
+		signal_bind().
+		"""
 		try:
-			handlers = self._signals[signal_handler.signal_name]
+			handlers = self._signals[signal_handler._signal_name]
 		except:
 			pass
 		else:
 			try:
+				signal_handler._function = None
 				handlers.remove(signal_handler)
 			except:
 				pass
 
 	def signal_garbage_collect(self):
+		"""
+		Remove all handlers with deleted weak references.
+
+		Usually this is not needed; every time you emit a signal, its handlers
+		are automatically checked in this way.  However, if you can't be sure
+		that a signal is ever emitted AND you keep binding weakly referenced
+		functions to the signal, this method should be regularly called to
+		avoid memory leaks in self._signals.
+
+		>>> sig = SignalDispatcher()
+
+		>>> # lambda:None is an anonymous function which has no references
+		>>> # so it should get deleted immediately
+		>>> handler = sig.signal_bind('test', lambda: None, weak=True)
+		>>> len(sig._signals['test'])
+		1
+		>>> # need to call garbage collect so that it's removed from the list.
+		>>> sig.signal_garbage_collect()
+		>>> len(sig._signals['test'])
+		0
+		>>> # This demonstrates that garbage collecting is not necessary
+		>>> # when using signal_emit().
+		>>> handler = sig.signal_bind('test', lambda: None, weak=True)
+		>>> sig.signal_emit('another_signal')
+		True
+		>>> len(sig._signals['test'])
+		1
+		>>> sig.signal_emit('test')
+		True
+		>>> len(sig._signals['test'])
+		0
+		"""
 		for handler_list in self._signals.values():
 			i = len(handler_list)
 			while i:
 				i -= 1
 				handler = handler_list[i]
 				try:
-					if isinstance(handler.function, tuple):
-						handler.function[1].__class__
+					if isinstance(handler._function, tuple):
+						handler._function[1].__class__
 					else:
-						handler.function.__class__
+						handler._function.__class__
 				except ReferenceError:
+					handler._function = None
 					del handler_list[i]
 
 	def signal_emit(self, signal_name, **kw):
+		"""
+		Emits a signal and call every function that was bound to that signal.
+
+		You can call this method with any key words.  They will be turned into
+		attributes of the Signal object that is passed to the functions.
+		If a function calls signal.stop(), no further functions will be called.
+		If a function raises a ReferenceError, the handler will be deleted.
+
+		Returns False if signal.stop() was called and True otherwise.
+		"""
 		assert isinstance(signal_name, str)
 		if signal_name not in self._signals:
 			return True
@@ -101,17 +255,23 @@ class SignalDispatcher(object):
 		# propagate
 		for handler in tuple(handlers):
 			if handler.active:
-				if isinstance(handler.function, tuple):
-					fnc = MethodType(*handler.function)
+				if isinstance(handler._function, tuple):
+					fnc = MethodType(*handler._function)
 				else:
-					fnc = handler.function
+					fnc = handler._function
 				try:
-					if handler.pass_signal:
+					if handler._pass_signal:
 						fnc(signal)
 					else:
 						fnc()
 				except ReferenceError:
+					handler._function = None
 					handlers.remove(handler)
 				if signal.stopped:
 					return False
 		return True
+
+
+if __name__ == '__main__':
+	import doctest
+	doctest.testmod()