# Event Signal
This library was created to help maintain when variables are changed and when functions are called.
There are 5 main utilities provided
* signaler - Function decorator to help observe functions
* signaler_property - Custom property that helps observe when a property value is changed or deleted.
* MethodObserver - class mixin to make all function observable
* Signal - Similar to Qt's signal without requiring PyQT or PySide
* bind - Make two object share the same value
## Use
There are 5 main functions to use the signals.
* get_signal - returns a list of connected function callbacks
* on - connect a callback function to a signal
* off - disconnect a callback funciton from a signal
* fire - Call all callback functions that are associated with a signal
* block - Temporarily block a signal from calling callback functions
Two basic signals are provided `'before_change'` and `'change'`. The signaler_property also has delete signals.
* 'before_change' - This signal automatically fires before a function is called.
* 'change' - This signal automatically fires after a function is called.
* 'before_delete' - signaler_property fired before `del obj.property` is called.
* 'delete' - signaler_property fired after `del obj.property` is called.
## Basics
A signaler is a custom class that decorates a function. It acts just like a function (it is callable). A signaler can
have other functions attached to it with a name. When the signaler fire is called it will call all of the attached
functions.
```python
from event_signal import signaler
def my_function(a, b, c):
print(a, b, c)
my_function = signaler(my_function)
# Call the signaler like a normal function
my_function(1, 2, 3)
# print = 1, 2, 3
print('=====\n')
def print_signal(value1, value2):
print('print_signal called', value1, value2)
my_function.on('my_signal', print_signal)
my_function.fire('my_signal', 1, 2)
# print = print_signal called 1 2
print('=====\n')
def another_signal(value1, value2):
print('another_signal called', value1, value2, value1 == value2)
my_function.on('my_signal', another_signal)
my_function.fire('my_signal', 2, 2)
# print = print_signal called 2 2
# print = another_signal called 2 2 True
print('=====\n')
my_function.off('my_signal', print_signal)
my_function.fire('my_signal', 3, 2)
# print = another_signal called 3 2 False
print('=====\n')
print(my_function.get_signal('my_signal'))
# print = [<function ...another_signal at 0x056076F0>]
print('=====\n')
my_function.block('my_signal')
my_function.fire('my_signal', 4, 5)
# No print!
my_function.block('my_signal', False)
my_function.fire('my_signal', 5, 6)
# print = another_signal called 5 6 False
print('=====\n')
```
## Example - signaler
Javascript like events for functions and objects.
The signaler automatically creates and fires signals `'before_change'` and `'change'`.
```python
from event_signal import signaler
class XTest(object):
def __init__(self, x=0):
self._x = x
def get_x(self):
return self._x
@signaler
def set_x(self, x):
self._x = x
@set_x.on("before_change")
def x_changing(self, x):
print("x is changing")
@set_x.on("change")
def x_changed(self, x):
print("x changed", x)
t = XTest()
t.set_x(1)
# x is changing
# x changed 1
t.set_x.on("change", lambda x: print("new signal"))
t.set_x(2)
# x is changing
# x changed 2
# new signal
t.set_x.off("before_change", t.x_changing)
t.set_x(3)
# x changed 3
# new signal
t.set_x.block()
t.set_x(4)
t.set_x.block(block=False)
t.set_x(5)
# x changed 3
# new signal
t.set_x.block('change', True)
t.set_x(6)
```
Change the value that is passed to the change callback functions.
```python
from event_signal import signaler
class XTest(object):
def __init__(self, x=0):
self._x = x
def get_x(self):
return self._x
@signaler(getter=get_x)
def set_x(self, x):
"""Set x and force the value to be between 1 and 100."""
if x < 0:
x = 0
elif x > 100:
x = 100
self._x = x
@set_x.on("before_change")
def x_changing(self, x):
print("x is changing", x)
@set_x.on("change")
def x_changed(self, x):
print("x changed", x)
t = XTest()
t.set_x(1)
# x is changing 1
# x changed 1
# Normally (without the getter) the change callback functions receive
# the x value that was passed into set_x
t.set_x(-1)
# x is changing -1
# x changed 0
t.set_x(102)
# x is changing 102
# x changed 100
# In this case the value passed into the change callback functions
# is the value returned from the signaler getter (t.get_x)
# which is how the signaler_property works.
```
## Example - signaler_property
A property with signaler capabilities.
The signaler_property automatically creates and fires signals `'before_change'`, `'change'`, `'before_delete'`, and `'delete'`.
```python
from event_signal import signaler_property
class XTest(object):
def __init__(self, x=0):
self._x = x
@signaler_property # or signaler.property
def x(self):
return self._x
@x.setter
def x(self, x):
self._x = x
@x.on("before_change")
def x_changing(self, x):
print("x is changing")
@x.on("change")
def x_changed(self, x):
print("x changed", x)
t = XTest()
t.x = 1
# x is changing
# x changed 1
XTest.x.on(t, "change", lambda x: print("new signal"))
t.x = 2
# x is changing
# x changed 2
# new signal
XTest.x.off(t, "before_change", t.x_changing)
t.x = 3
# x changed 3
# new signal
XTest.x.block(t, 'change')
t.x = 4
XTest.x.block(t, 'change', False)
t.x = 5
# x changed
# new signal
```
## Example - MethodObserver
Inheritable class or metaclass that makes every function/method in a class a signaler.
```python
from event_signal import MethodObserver
class XTest(MethodObserver):
def __init__(self, x=0):
self._x = x
def get_x(self):
return self._x
def set_x(self, x):
self._x = x
def x_changing(self, x):
print("x is changing")
def x_changed(self, x):
print("x changed", x)
t = XTest()
t.set_x(1)
t.set_x.on("change", t.x_changed)
t.set_x(2)
# x changed 2
t.set_x.on("before_change", t.x_changing)
t.set_x(3)
# x is changing
# x changed 3
t.set_x.off("before_change", t.x_changing)
t.set_x(4)
# x changed 4
```
## Example - Signal
Qt like signal.
**Warning:**
Qt's signals are thread safe (depending on how you connect them). They call the callback functions in the main thread.
Many Qt widgets error and do not update when a value is set from a separate thread. So Qt's Signal is a good way to
update a QWidget's value display, but can be slow.
The event_signal.Signal works like a Qt Signal with a direct connection.
The callback functions are called in the same thread that originally called the function. If you are using Qt and use
this Signal from a separate thread to udate a QWidget it may not work properly and throw errors or warnings.
For more information on how to make a QWidget thread safe go to <https://tasks.justengel.com/project/justengel-event-signal/wiki/signal-qt-thread-safe>
Also this signal does not do any kind of type checking. Passing types into the Signal constructor ```Signal(int, str)```
is just for looks and maybe code readability.
```python
from event_signal import Signal
class XTest(object):
x_changed = Signal(int)
def __init__(self, x=0):
self._x = x
def get_x(self):
return self._x
def set_x(self, x):
self._x = x
self.x_changed.emit(self._x)
def notify_x_changed(self, x):
print("x changed", x)
t = XTest()
t.set_x(1)
t.x_changed.connect(t.notify_x_changed)
t.set_x(2)
# x changed 2
t.x_changed.emit(3)
# x changed 3
print(t.get_x())
# 2
t.x_changed.disconnect(t.notify_x_changed) # or t.x_changed.disconnect()
t.set_x(4)
print(t.get_x())
# 4
```
## Example - bind
bind the value of two objects together. This will automatically use properties or find setter methods
("set_" + property_name or "set" + property_name). The binder will change a property to a signaler_property or if a
property is not found and a setter function is used it will change that setter function to a signaler if it is not
already a signaler.
The main goal is to help two objects keep the same value for a variable.
When using Qt I found this very annoying. I wanted a regular python object to store data and a GUI Widget to display
the value and let the user change value. I wanted the two items decoupled. Occasionally, I wanted to programmatically
set the data object value and wanted the GUI Widget to display this change automatically. The signals became annoying
to deal with since I do a lot of work with threading. After several overly complex solutions, I made this bind function
to make the GUI and data objects match values.
```python
from event_signal import bind, bind_signals # bind_signals is only for directly giving signalers.
class XTest(object):
def __init__(self, x=0, y=0):
self._x = x
self._y = y
def get_x(self):
return self._x
def set_x(self, x):
self._x = x
@property
def y(self):
return self._y
@y.setter
def y(self, y):
self._y = y
t = XTest()
t2 = XTest()
bind(t, "x", t2)
t.set_x(1)
print(t.get_x())
# 1
assert t.get_x() == t2.get_x()
bind(t, "y", t2, "y")
t2.y = 2
print(t2.y)
# 2
assert t.y == t2.y
```
You can manually bind the signalers as well.
```python
from event_signal import signaler, bind_signals # bind_signals is only for directly giving signalers.
class Test2(object):
def __init__(self, x=0, y=0):
self._x = x
self._y = y
def get_x(self):
return self._x
@signaler(getter=get_x)
def set_x(self, x):
self._x = x
t1 = Test2()
t2 = Test2()
bind_signals(t1.set_x, t2.set_x)
t1.set_x(2)
assert t1.get_x() == t2.get_x()
t2.set_x(5)
assert t1.get_x() == t2.get_x()
```
An unbind option is also available and works just like the examples above accept you can choose to unbind a single object at a time
```python
from event_signal import bind, unbind, unbind_signals # unbind_signals is only for directly giving one or more signalers.
class XTest(object):
def __init__(self, x=0):
self._x = x
def get_x(self):
return self._x
def set_x(self, x):
self._x = x
t = XTest()
t2 = XTest()
bind(t, "x", t2)
t.set_x(1)
print(t.get_x())
# 1
assert t.get_x() == t2.get_x()
unbind(t, "x")
t.set_x(2)
print(t.get_x())
# 2
assert t.get_x() != t2.get_x()
t2.set_x(3)
print(t2.get_x())
# 3
assert t.get_x() == t2.get_x()
unbind(t2.set_x)
t2.set_x(4)
print(t2.get_x())
# 4
assert t.get_x() != t2.get_x()
bind(t, "x", t2)
t.set_x(1)
print(t.get_x())
# 1
assert t.get_x() == t2.get_x()
unbind(t, "x", t2)
t.set_x(2)
print(t.get_x())
# 2
assert t.get_x() != t2.get_x()
t2.set_x(3)
print(t2.get_x())
# 3
assert t.get_x() != t2.get_x()
```
### bind Qt
I now provide a `bind_qt` and `unbind_qt` functions.
```python
from event_signal import signaler, bind_qt, unbind_qt
from qtpy import QtWidgets
class MyData(object):
def __init__(self, name='hello'):
self._name = name
def get_name(self):
return self._name
@signaler(getter=get_name)
def set_name(self, name):
self._name = str(name)
app = QtWidgets.QApplication([])
widg = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout()
widg.setLayout(lay)
widg.show()
data = MyData()
data.set_name.on('change', lambda name: print('data name changed to', name))
inp = QtWidgets.QLineEdit('Hello World!')
lay.addWidget(inp)
bind_qt(data, 'set_name', inp, 'setText') # qt_signal='editingFinished'
btn = QtWidgets.QPushButton('Set Hello')
def set_hello():
data.set_name('Hello')
btn.clicked.connect(set_hello)
lay.addWidget(btn)
unbind_btn = QtWidgets.QPushButton('unbind')
def unbind_call():
unbind_qt(data, 'set_name', inp, 'setText')
unbind_btn.clicked.connect(unbind_call)
lay.addWidget(unbind_btn)
app.exec_()
```
To edit what widget value is used.
```python
from event_signal import qt_binder
def get_widget_value(widget):
if isinstance(widget, CustomWidget):
return widget.my_value()
return qt_binder.get_widget_value(widget)
qt_binder.get_widget_value = get_widget_value()
```
One thing to watch out for is if functions are called multiple times or are infinitely recursive.