summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorhut <hut@lavabit.com>2011-10-07 20:26:38 +0200
committerhut <hut@lavabit.com>2011-10-07 21:31:23 +0200
commitaa717a186e793b1972a0a62b76353c399d4ae1c0 (patch)
tree23a795ddcfaadc5535227ea62987f18aa546eaa0
parent5fca2a0b63bb565ab4faba135dc1d201c3b05725 (diff)
downloadranger-aa717a186e793b1972a0a62b76353c399d4ae1c0.tar.gz
ext.signals: Fixed hidden bugs, added doctests
-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()
='#n559'>559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646