signal.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on Mon Mar 11 18:39:13 2013
  4. @author Vaclav Petras <wenzeslaus gmail.com>
  5. """
  6. from grass.pydispatch import dispatcher
  7. def _islambda(function):
  8. """
  9. Tests if object is a lambda function.
  10. Should work on the most of Python implementations where name of lambda
  11. function is not unique.
  12. >>> mylambda = lambda x: x*x
  13. >>> _islambda(mylambda)
  14. True
  15. >>> _islambda(_islambda)
  16. False
  17. """
  18. return isinstance(function, type(lambda: None)) and function.__name__== (lambda: None).__name__
  19. class Signal(object):
  20. """
  21. The signal object is created usually as a instance attribute.
  22. However, it can be created anywhere.
  23. >>> signal1 = Signal('signal1')
  24. The function has to be connected to a signal in order to be called when
  25. the signal is emitted. The connection can be done where the function is
  26. defined (e. g., a class) but also on some other place, typically,
  27. user of a class connects some signal to the method of some other class.
  28. >>> def handler1():
  29. ... print "from handler1"
  30. >>> signal1.connect(handler1)
  31. Emitting of the signal is done usually only in the class which has the
  32. signal as a instance attribute. Again, generally, it can be emitted
  33. anywhere.
  34. >>> signal1.emit()
  35. from handler1
  36. The signal can have parameters. These parameters are specified when
  37. emitting but should be documented together with the signal (e.g., in the
  38. class documentation). Parameters should be keyword arguments and handlers
  39. must use these names (if the names cannot be the same, lambda function
  40. can be used to overcome this problem).
  41. >>> signal2 = Signal('signal2')
  42. >>> def handler2(text):
  43. ... print "handler2: %s" % text
  44. >>> signal2.connect(handler2)
  45. >>> signal2.emit(text="Hello")
  46. handler2: Hello
  47. Do not emit the same signal with different parameters when emitting at
  48. different places.
  49. A handler is the standard function, lambda function, method or any other
  50. callable object.
  51. >>> import sys
  52. >>> signal2.connect(lambda text:
  53. ... sys.stdout.write('lambda handler: %s\\n' % text))
  54. >>> signal2.emit(text="Hi")
  55. handler2: Hi
  56. lambda handler: Hi
  57. The handler function can have only some of the signal parameters or no
  58. parameters at all even if the signal has some.
  59. >>> def handler3():
  60. ... print "from handler3"
  61. >>> signal2.connect(handler3)
  62. >>> signal2.emit(text="Ciao")
  63. handler2: Ciao
  64. lambda handler: Ciao
  65. from handler3
  66. It is possible to use signal as a handler. By this, signals can be
  67. forwarded from one object to another. In other words, one object can
  68. expose signal of some object.
  69. >>> signal3 = Signal('signal3')
  70. >>> signal3.connect(handler3)
  71. >>> signal1.connect(signal3)
  72. >>> signal1.emit()
  73. from handler1
  74. from handler3
  75. It is possible to disconnect a particular handler.
  76. >>> signal3.disconnect(handler3)
  77. >>> signal1.emit()
  78. from handler1
  79. >>> signal2.disconnect(handler2)
  80. >>> signal2.disconnect(handler3)
  81. >>> signal2.emit(text='Hello')
  82. lambda handler: Hello
  83. """
  84. # TODO: use the name for debugging
  85. def __init__(self, name):
  86. """Creates a signal object.
  87. The parameter name is used for debugging.
  88. """
  89. self._name = name
  90. def connect(self, handler, weak=None):
  91. """
  92. Connects handler to a signal.
  93. Typically, a signal is defined in some class and the user of this
  94. class connects to the signal::
  95. from module import SomeClass
  96. ...
  97. self.someObject = SomeClass()
  98. self.someObject.connect(self.someMethod)
  99. Usually, it is not needed to set the weak parameter. This method
  100. creates weak references for all handlers but for lambda functions, it
  101. automaticaly creates (standard) references (otherwise, lambdas would be
  102. garbage collected. If you want to force some behaviour, specify the
  103. weak parameter.
  104. >>> signal1 = Signal('signal1')
  105. >>> import sys
  106. >>> signal1.connect(lambda: sys.stdout.write('will print\\n'))
  107. >>> signal1.connect(lambda: sys.stdout.write('will print\\n'), weak=False)
  108. >>> signal1.connect(lambda: sys.stdout.write('will not print'), weak=True)
  109. >>> signal1.emit()
  110. will print
  111. will print
  112. """
  113. if weak is None:
  114. if _islambda(handler):
  115. weak = False
  116. else:
  117. weak = True
  118. dispatcher.connect(receiver=handler, signal=self, weak=weak)
  119. def disconnect(self, handler, weak=True):
  120. """
  121. Disconnects a specified handler.
  122. It is not necessary to disconnect object when it is deleted.
  123. Underlying PyDispatcher will take care of connections to deleted
  124. objects.
  125. >>> signal1 = Signal('signal1')
  126. >>> import sys
  127. >>> signal1.connect(sys.stdout.write)
  128. >>> signal1.disconnect(sys.stdout.write)
  129. The weak parameter of must have the same value as for connection.
  130. If you not specified the parameter when connecting,
  131. you don't have to specify it when disconnecting.
  132. Disconnecting the not-connected handler will result in error.
  133. >>> signal1.disconnect(sys.stdout.flush) #doctest: +ELLIPSIS
  134. Traceback (most recent call last):
  135. DispatcherKeyError: 'No receivers found for signal <__main__.Signal object at 0x...> from sender _Any'
  136. Disconnecting the non-exiting or unknown handler will result in error.
  137. >>> signal1.disconnect(some_function)
  138. Traceback (most recent call last):
  139. NameError: name 'some_function' is not defined
  140. >>> signal1.emit()
  141. """
  142. dispatcher.disconnect(receiver=handler, signal=self, weak=weak)
  143. # TODO: remove args?, make it work for args?
  144. # TODO: where to put documentation
  145. def emit(self, *args, **kwargs):
  146. """
  147. Emits the signal which means that all connected handlers will be
  148. called.
  149. It is advised to have signals as instance attributes and emit signals
  150. only in the class which owns the signal::
  151. class Abc(object):
  152. def __init__(self):
  153. self.colorChanged = Signal('Abc.colorChanged')
  154. ...
  155. def setColor(self, color):
  156. ...
  157. self.colorChanged.emit(oldColor=self.Color, newColor=color)
  158. ...
  159. Documentation of an signal should be placed to the class documentation
  160. or to the code (this need to be more specified).
  161. Calling a signal from outside the class is usually not good
  162. practice. The only case when it is permitted is when signal is the part
  163. of some globaly shared object and permision to emit is stayed in the
  164. documentation.
  165. The parameters of the emit function must be the same as the parameters
  166. of the handlers. However, handler can ommit some parameters.
  167. The associated parameters shall be documented for each Signal instance.
  168. Use only keyword arguments when emitting.
  169. >>> signal1 = Signal('signal1')
  170. >>> def mywrite(text):
  171. ... print text
  172. >>> signal1.connect(mywrite)
  173. >>> signal1.emit(text='Hello')
  174. Hello
  175. >>> signal1.emit()
  176. Traceback (most recent call last):
  177. TypeError: mywrite() takes exactly 1 argument (0 given)
  178. >>> signal1.emit('Hello')
  179. Traceback (most recent call last):
  180. TypeError: send() got multiple values for keyword argument 'signal'
  181. """
  182. dispatcher.send(signal=self, *args, **kwargs)
  183. # TODO: remove args?
  184. def __call__(self, *args, **kwargs):
  185. """Allows to emit signal with function call syntax.
  186. It allows to handle signal as a function or other callable object.
  187. So, the signal can be in the list of fuctions or can be connected as
  188. a handler for another signal.
  189. However, it is strongly recommended to use emit method for direct
  190. signal emitting.
  191. The use of emit method is more explicit than the function call
  192. and thus it it clear that we are using signal.
  193. >>> signal1 = Signal('signal1')
  194. >>> def mywrite(text):
  195. ... print text
  196. >>> signal1.connect(mywrite)
  197. >>> functions = [signal1, lambda text: mywrite(text + '!')]
  198. >>> for function in functions:
  199. ... function(text='text')
  200. text
  201. text!
  202. The other reason why the function call should not by used when it is
  203. possible to use emit method is that this function does ugly hack to
  204. enable calling as a signal handler. The signal parameter is deleted
  205. when it is in named keyword arguments. As a consequence, when the
  206. signal is emitted with the signal parameter (which is a very bad
  207. name for parameter when using signals), the error is much more readable
  208. when using emit than function call. Concluding remark is that
  209. emit behaves more predictable.
  210. >>> signal1.emit(signal='Hello')
  211. Traceback (most recent call last):
  212. TypeError: send() got multiple values for keyword argument 'signal'
  213. >>> signal1(signal='Hello')
  214. Traceback (most recent call last):
  215. TypeError: mywrite() takes exactly 1 argument (0 given)
  216. """
  217. if 'signal' in kwargs:
  218. del kwargs['signal']
  219. self.emit(*args, **kwargs)
  220. if __name__ == '__main__':
  221. import doctest
  222. doctest.testmod()