Pure python implementation of Qt Signals
Project description
psygnal
Pure python implementation of Qt-style Signals, with (optional) signature and type checking, and support for threading.
Note: this library does not require Qt. It just implements a similar pattern of inter-object communication with loose coupling.
Usage
Install
pip install psygnal
Basic usage
If you are familiar with the Qt Signals &
Slots API as implemented in
PySide and
PyQt5,
then you should be good to go! psygnal
aims to be a superset of those APIs
(some functions do accept additional arguments, like
check_nargs
and
check_types
).
Note: the name "Signal
" is used here instead of pyqtSignal
, following the
qtpy
and PySide
convention.
from psygnal import Signal
# create an object with class attribute Signals
class MyObj:
# this signal will emit a single string
value_changed = Signal(str)
def __init__(self, value=0):
self._value = value
def set_value(self, value):
if value != self._value:
self._value = str(value)
# emit the signal
self.value_changed.emit(self._value)
def on_value_changed(new_value):
print(f"The new value is {new_value!r}")
# instantiate the object with Signals
obj = MyObj()
# connect one or more callbacks with `connect`
obj.value_changed.connect(on_value_changed)
# callbacks are called when value changes
obj.set_value('hello!') # prints: 'The new value is 'hello!'
# disconnect callbacks with `disconnect`
obj.value_changed.disconnect(on_value_changed)
connect
as a decorator
.connect()
returns the object that it is passed, and so
can be used as a decorator.
@obj.value_changed.connect
def some_other_callback(value):
print(f"I also received: {value!r}")
obj.set_value('world!')
# prints:
# I also received: 'world!'
Connection safety (number of arguments)
psygnal
prevents you from connecting a callback function that is guaranteed
to fail due to an incompatible number of positional arguments. For example,
the following callback has too many arguments for our Signal (which we declared
above as emitting a single argument: Signal(str)
)
def i_require_two_arguments(first, second):
print(first, second)
obj.value_changed.connect(i_require_two_arguments)
raises:
ValueError: Cannot connect slot 'i_require_two_arguments' with signature: (first, second):
- Slot requires at least 2 positional arguments, but spec only provides 1
Accepted signature: (p0: str, /)
Note: Positional argument checking can be disabled with connect(..., check_nargs=False)
Extra positional arguments ignored
While a callback may not require more positional arguments than the signature
of the Signal
to which it is connecting, it may accept less. Extra
arguments will be discarded when emitting the signal (so it
isn't necessary to create a lambda
to swallow unnecessary arguments):
obj = MyObj()
def no_args_please():
print(locals())
obj.value_changed.connect(no_args_please)
# otherwise one might need
# obj.value_changed.connect(lambda a: no_args_please())
obj.value_changed.emit('hi') # prints: "{}"
Connection safety (types)
For type safety when connecting slots, use check_types=True
when connecting a
callback. Recall that our signal was declared as accepting a string
Signal(str)
. The following function has an incompatible type annotation: x: int
.
# this would fail because you cannot concatenate a string and int
def i_expect_an_integer(x: int):
print(f'{x} + 4 = {x + 4}')
# psygnal won't let you connect it
obj.value_changed.connect(i_expect_an_integer, check_types=True)
raises:
ValueError: Cannot connect slot 'i_expect_an_integer' with signature: (x: int):
- Slot types (x: int) do not match types in signal.
Accepted signature: (p0: str, /)
Note: unlike Qt, psygnal
does not perform any type coercion when emitting
a value.
Connection safety (object references)
psygnal tries very hard not to hold strong references to connected objects. In the simplest case, if you connect a bound method as a callback to a signal instance:
class T:
def my_method(self):
...
obj = T()
signal.connect(t.my_method)
Then there is a risk of signal
holding a reference to obj
even after obj
has been deleted, preventing garbage collection (and possibly causing errors
when the signal is emitted next). Psygnal avoids this with weak references. It
goes a bit farther, trying to prevent strong references in these cases as well:
- class methods used as the callable in
functools.partial
- decorated class methods that mangle the name of the callback.
Another common case for leaking strong references is a partial
closing on an
object, in order to set an attribute:
class T:
x = 1
obj = T()
signal.connect(partial(setattr, obj, 'x')) # ref to obj stuck in the connection
Here, psygnal offers the connect_settatr
convenience method, which reduces code
and helps you avoid leaking strong references to obj
:
signal.connect_setatttr(obj, 'x')
Query the sender
Similar to Qt's QObject.sender()
method, a callback can query the sender using the Signal.sender()
class
method. (The implementation is of course different than Qt, since the receiver
is not a QObject
.)
obj = MyObj()
def curious():
print("Sent by", Signal.sender())
assert Signal.sender() == obj
obj.value_changed.connect(curious)
obj.value_changed.emit(10)
# prints (and does not raise):
# Sent by <__main__.MyObj object at 0x1046a30d0>
If you want the actual signal instance that is emitting the signal
(obj.value_changed
in the above example), use Signal.current_emitter()
.
Emitting signals asynchronously (threading)
There is experimental support for calling all connected slots in another thread,
using emit(..., asynchronous=True)
obj = MyObj()
def slow_callback(arg):
import time
time.sleep(0.5)
print(f"Hi {arg!r}, from another thread")
obj.value_changed.connect(slow_callback)
This one is called synchronously (note the order of print statements):
obj.value_changed.emit('friend')
print("Hi, from main thread.")
# after 0.5 seconds, prints:
# Hi 'friend', from another thread
# Hi, from main thread.
This one is called asynchronously, and immediately returns to the caller.
A threading.Thread
object is returned.
thread = obj.value_changed.emit('friend', asynchronous=True)
print("Hi, from main thread.")
# immediately prints
# Hi, from main thread.
# then after 0.5 seconds this will print:
# Hi 'friend', from another thread
Note: The user is responsible for joining
and managing the
threading.Thread
instance returned when calling .emit(..., asynchronous=True)
.
Experimental! While thread-safety is the goal,
(RLocks
are
used during important state mutations) it is not guaranteed. Please use at your
own risk. Issues/PRs welcome.
Blocking a signal
To temporarily block a signal, use the signal.blocked()
context context manager:
obj = MyObj()
with obj.value_changed.blocked():
# do stuff without obj.value_changed getting emitted
...
To block/unblock permanently (outside of a context manager), use signal.block()
and signal.unblock()
.
Pausing a signal
Sometimes it is useful to temporarily collect/buffer emission events, and then emit
them together as a single event. This can be accomplished using the
signal.pause()
/signal.resume()
methods, or the signal.paused()
context manager.
If a function is passed to signal.paused(func)
(or signal.resume(func)
) it will
be passed to functools.reduce
to combine all of the emitted values collected during
the paused period, and a single combined value will be emitted.
obj = MyObj()
obj.value_changed.connect(print)
# note that signal.paused() and signal.resume() accept a reducer function
with obj.value_changed.paused(lambda a,b: (f'{a[0]}_{b[0]}',), ('',)):
obj.value_changed('a')
obj.value_changed('b')
obj.value_changed('c')
# prints '_a_b_c'
NOTE: args passed to emit
are collected as tuples, so the two arguments
passed to reducer
will always be tuples. reducer
must handle that and
return an args tuple.
For example, the three emit()
events above would be collected as
[('a',), ('b',), ('c',)]
and would be reduced and re-emitted as follows:
obj.emit(*functools.reduce(reducer, [('a',), ('b',), ('c',)]))
Other similar libraries
There are other libraries that implement similar event-based signals, they may server your purposes better depending on what you are doing.
PySignal (deprecated)
This package borrows inspiration from – and is most similar to – the now
deprecated PySignal project, with a few
notable new features in psygnal
regarding signature and type checking, sender
querying, and threading.
similarities with PySignal
- still a "Qt-style" signal implementation that doesn't depend on Qt
- supports class methods, functions, lambdas and partials
differences with PySignal
- the class attribute
pysignal.ClassSignal
is called simplySignal
inpsygnal
(to more closely match the PyQt/Pyside syntax). Correspondinglypysignal.Signal
is similar topsygnal.SignalInstance
. - Whereas
PySignal
refrained from doing any signature and/or type checking either at slot-connection time, or at signal emission time,psygnal
offers signature declaration similar to Qt with , for example,Signal(int, int)
. along with opt-in signature compatibility (withcheck_nargs=True
) and type checking (withcheck_types=True
)..connect(..., check_nargs=True)
in particular ensures that any slot to connected to a signal will at least be compatible with the emitted arguments. - You can query the sender in
psygnal
by using theSignal.sender()
orSignal.current_emitter()
class methods. (The former returns the instance emitting the signal, similar to Qt'sQObject.sender()
method, whereas the latter returns the currently emittingSignalInstance
.) - There is basic threading support (calling all slots in another thread), using
emit(..., asynchronous=True)
. This is experimental, and while thread-safety is the goal, it is not guaranteed. - There are no
SignalFactory
classes here.
The following two libraries implement django-inspired signals, they do not attempt to mimic the Qt API.
Blinker
Blinker provides a fast dispatching system that allows any number of interested parties to subscribe to events, or "signals".
SmokeSignal
(This appears to be unmaintained)
Benchmark history
https://www.talleylambert.com/psygnal/
Developers
Debugging
While psygnal
is a pure python module, it is compiled with Cython to increase
performance. To import psygnal
in uncompiled mode, without deleting the
shared library files from the psyngal module, set the environment variable
PSYGNAL_UNCOMPILED
before importing psygnal. The psygnal._compiled
variable
will tell you if you're running the compiled library or not.
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
Built Distributions
Hashes for psygnal-0.3.4-cp310-cp310-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0621bf6c321ca037eb361963461376370f6e36179b02e436f1078a5189847ead |
|
MD5 | 69ff8d6b0511f2a36c2ddb53a36aa0b1 |
|
BLAKE2b-256 | fa0f301ba526554483da0c49e5db3a3fbf659b5e8f5d35b2a30287250c171810 |
Hashes for psygnal-0.3.4-cp310-cp310-musllinux_1_1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | c4aded28e0b468a82680216bed18110078982280c3dd99af9626e6641f72277d |
|
MD5 | d42dd67827d0b2b20f02e224d31124fa |
|
BLAKE2b-256 | 6ab3b39b2b2ff00316eff26aae01118b0a44a89e7c87bde2c5854e48e452d59a |
Hashes for psygnal-0.3.4-cp310-cp310-musllinux_1_1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | db7a30657eda2aafcc5fe31e6d9a6381c22690ce33850eb5314073850a60c879 |
|
MD5 | d5831152a19921f33834da085b32230d |
|
BLAKE2b-256 | 75ffdb6a9934321c436f766474b21d92da11b62ae0d35962f7ee1ac2d6252214 |
Hashes for psygnal-0.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | fcc685fbebaf75380746832494288a1874d68aee845fa7f3ca71dbf60efc213c |
|
MD5 | 5571158627322859b87694297bf5fb27 |
|
BLAKE2b-256 | 3c1a03ab83c84ac778f78eb0cbe4235436a440ec60b09b963e443fd261857a21 |
Hashes for psygnal-0.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | f07d87bdde5cb259b294b0ff2e2191c95551a3cbd1b2c1b2a82f05e3781e9f5d |
|
MD5 | 140f0a795bdaea2ec4b65fc486779643 |
|
BLAKE2b-256 | f3e4d698e444647ad5b92f40b5d0e8ba69519308ee1c78a5571d9804a28facef |
Hashes for psygnal-0.3.4-cp310-cp310-macosx_11_0_arm64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 298966ec82472980a5df5127a9ff6a435c6794b399c78e2ad842a640617e51a6 |
|
MD5 | 847ef89f4b6c60127732b9aa8ff0f52b |
|
BLAKE2b-256 | 1dbeddbd81de4d5a47b6bd01e4001cbd61d5e5530004263767dda06c65308ae0 |
Hashes for psygnal-0.3.4-cp310-cp310-macosx_10_9_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 3133457da8352f15c94a48af466bcfdf3aa7b18f90af56aea63a7b04b70cc7e2 |
|
MD5 | ea09c0c063e412b753f2df7312cd803e |
|
BLAKE2b-256 | 75d02f465cef3297fafa67f04bcee280bd8cf3d46d2c824f58c419ed6d263e89 |
Hashes for psygnal-0.3.4-cp39-cp39-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0f18e4b863f31605eeb58bd75833c46ac3ec349820a22f25d51022492ecd23c1 |
|
MD5 | 50af96885e77c261621303b93a9daf7f |
|
BLAKE2b-256 | effb4abfbac4de5383c8b824b1a880026e24eeb9da72b357b0ed6607f8acdf03 |
Hashes for psygnal-0.3.4-cp39-cp39-musllinux_1_1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 34899f0b7a4e2cfcc8b09bc112b314573bdb7d86799cc90b13f91303e376eb2f |
|
MD5 | 2c2482906face19c32dff0ff02044066 |
|
BLAKE2b-256 | f78a28bb5e91c641e42b68806073e397d7d40fbbb5f56383153e3e8dfd5112a0 |
Hashes for psygnal-0.3.4-cp39-cp39-musllinux_1_1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 659845f7b2c0e681b7baaea81fc27179b49faac80386bcc1367c87ac5b64a1e1 |
|
MD5 | 9a2fbec5195122d6fc8754daa32df0ad |
|
BLAKE2b-256 | 33b2028d4077cf980bc7d26005478929079e40131d02bf28cef45d75519d2a2a |
Hashes for psygnal-0.3.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 063c0f0e74031b8dfec9bc38674bf7db5e6e8106f4fcaa93f3db05ffdb02a603 |
|
MD5 | 33bcac09777a7a49291a02ce92893889 |
|
BLAKE2b-256 | e215abd1814bb8f54891bc45eaca401816178b8d358f646fdf61674e45c91adb |
Hashes for psygnal-0.3.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2fa28532dc4e2a8bffb8ff8e1879740839e2a1b617f0cbe7b1ce17a42ef20aca |
|
MD5 | de9de3c92d8496d7c9c014e3f9a66624 |
|
BLAKE2b-256 | 68fd9b8041848e785e6280c5dd2a44ba9e0e0f05daf6eb4592460dc8b9815e34 |
Hashes for psygnal-0.3.4-cp39-cp39-macosx_11_0_arm64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | d097d69c72ac8b344e6e48596716078a60e0feebeb2ac2b2da7bf599b6e1fa78 |
|
MD5 | 47b1f5118257b57c3dbfe92a56a94fc9 |
|
BLAKE2b-256 | 80e60952485887bcd553b87ecf73a0320543e8e0927167e8d1e96b2b76937e2f |
Hashes for psygnal-0.3.4-cp39-cp39-macosx_10_9_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 65da6fb93a5a8f17bc8894e3381fa736ba8c36eb66e394ce48884ed25c2839ea |
|
MD5 | 4ffc5d3732581460ca6eb2bf752339f3 |
|
BLAKE2b-256 | 76383be810eb83588cfb70d23933d583ea7efd20c3fe24e3e375d37f37d95062 |
Hashes for psygnal-0.3.4-cp38-cp38-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | b38f0ce40815f7816a1bd8a69c90e22d2b7e872595711ba0079fe08407d760cd |
|
MD5 | 0a37f4d4fa882a9fdedb449e0821c1a2 |
|
BLAKE2b-256 | d48666f866d3ba3926120c802afc9a3c2f6226acdadfbace1dbf29bcb2972d6c |
Hashes for psygnal-0.3.4-cp38-cp38-musllinux_1_1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ad4c66e09a06947082cdffca36411e5a883a02b69e29680967a5c6d451596f24 |
|
MD5 | 0bc6c48eebf6634df790a8b7db40ad11 |
|
BLAKE2b-256 | 20632dcae2c6fa76fcde79a43e2a32b61941beb47dac2bb811056024eaeba92e |
Hashes for psygnal-0.3.4-cp38-cp38-musllinux_1_1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5962cd6a44e8f5dad7c75b1cebf6c170ba426fab10e4281942a531e20c4aab8b |
|
MD5 | bec052af40c203a307aae2d801d1fee0 |
|
BLAKE2b-256 | d3ef3d1d702e91eb81595d0984c809cc3ee60a7a20726327ac18ebf6321a1c86 |
Hashes for psygnal-0.3.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4bfc5e6662a9fdf0d7ec63a40d6f1a06f70880362b3c554553ccd51f97f34b8c |
|
MD5 | 89f6371ce3e19f2e3dd8a494a572a853 |
|
BLAKE2b-256 | 394a20c2bb20b5ef0e6c9547d7921ddaf3f655c216772d459570e75b94dad83e |
Hashes for psygnal-0.3.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | a73a332451404913fb8043db6e071cb5b07c6b9bbded5f37bb31e854b258e3cb |
|
MD5 | c83d7cc8641f60f9de2e2e0d819a9ed5 |
|
BLAKE2b-256 | ed03765c1ef9324120c48ed8a01208246fc6a2341a112099141e682852bd38b9 |
Hashes for psygnal-0.3.4-cp38-cp38-macosx_11_0_arm64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | f0ba783879e6d47b935336e5dfe290a3ff243b5aa55a7b1ef45fa14f762c45ea |
|
MD5 | 8ddc26ab079d2b4a48e2831682c36b6f |
|
BLAKE2b-256 | 8b034ab03248497ebb612804f2a219e1e0bc4acba5c1353900e3ef652f3a0eb7 |
Hashes for psygnal-0.3.4-cp38-cp38-macosx_10_9_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 37b30a1d3596c1cf9aaaf55124e0ebedcab56de8df303632ea88c23e5f2d40dc |
|
MD5 | 4f1d2c8127fcdddac815161e79cfdf87 |
|
BLAKE2b-256 | a7abaf9eb8e4aa8dc7e80f48a63aa7bbe6a5a49d5e12dda4cd4ab22103bf0cf1 |
Hashes for psygnal-0.3.4-cp37-cp37m-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | e022263d659c1198c88dbd145a1c1486007669b13fd2f7d73c3599f4c94d94ff |
|
MD5 | 6b31585392676469ad4eea235d3c0d05 |
|
BLAKE2b-256 | cc6218dd8fab18a68f61594ea8ed9b493330adc55b462a3407afc6b5af2269fc |
Hashes for psygnal-0.3.4-cp37-cp37m-musllinux_1_1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0eaffc5b2d0b19438138387a6421c4847d4273980aff92f26795febd6b72f9f7 |
|
MD5 | 844b7d84a679c63ae98b9eb91a77bff9 |
|
BLAKE2b-256 | deef89a74068e995132e2d5ae42855cecaf9165aae93ee2336b7e5ed4b557b98 |
Hashes for psygnal-0.3.4-cp37-cp37m-musllinux_1_1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8452a1fb2edd4677a6987875afb68492954832e6a9a46534cd4346c3b9b63d83 |
|
MD5 | 092547e19c1c377972865cbdca6e238d |
|
BLAKE2b-256 | 90f942f6f539140458abddac216656cf2c8ef7659449849a9d60fbfdd231882c |
Hashes for psygnal-0.3.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ecf533efa143e3ffd7c547d21a0b9ed1f379159e822500c37c15539e21c00b1c |
|
MD5 | bc0b46201e187206daa9058644a26510 |
|
BLAKE2b-256 | 6d7f7fd1ffc284b4317f352d25f5af3c4171c93639b132ddad6b1a1f1ee0aa12 |
Hashes for psygnal-0.3.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1b98e39bbec4f4e9b695063bbda2b7408587d18c78414731091654106de79433 |
|
MD5 | 06733b171a719fe7cb35e46643fd39d7 |
|
BLAKE2b-256 | dc61e418d2946abbd9fc9d2b9656cd20d185306c908580e8311d6c68aae4ff64 |
Hashes for psygnal-0.3.4-cp37-cp37m-macosx_10_9_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ff2c0561f55790888a49caafe1e23f9f8e17601ee5f2cde622d4d2ee26a02e85 |
|
MD5 | 35c5aa4b54263a9c1384938f13cb102a |
|
BLAKE2b-256 | 665328c2815bb44b7b280b4ef35ed7b0b5cc8279fbcd776ef9c9db0c7ed4a53b |