Library to help notify when something has changed.
Project description
# 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.
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
event_signal-1.8.0.tar.gz
(19.8 kB
view details)
File details
Details for the file event_signal-1.8.0.tar.gz
.
File metadata
- Download URL: event_signal-1.8.0.tar.gz
- Upload date:
- Size: 19.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8a2865573c53507da66cde2eb119a79fcf704835c6dbc87a20126e8d30fad013 |
|
MD5 | 582a3fd64ef27c58c0b7a2eda789d9ee |
|
BLAKE2b-256 | 94deccbe6b572c3303a02eba9c1b9f5465afd113abe84f0ee95b2febcd6300ee |