SignalProxy: Correct initialization without slot argument and tests (#1392)

* SignalProxy: Correct initialization without slot argument and provide tests

* Add missing slot is None case on disconnect

* Start new tests

* Exception block

* Test no module

* Different signal

* Debugging the signal connect

* Re initialize proxy after disconnect

* Add more test cases for blockSignal

* Change test case for signal count

* Give up for python 2 and pyside

* Exclude for Python 2.7 and PySide

* Convert float to integers in timer start period
This commit is contained in:
Dennis Göries 2020-10-13 17:50:22 +02:00 committed by GitHub
parent 4946a57987
commit 325a15a6ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 191 additions and 40 deletions

View File

@ -1,23 +1,26 @@
# -*- coding: utf-8 -*-
import weakref
from .Qt import QtCore
from .ptime import time
from . import ThreadsafeTimer
import weakref
from .functions import SignalBlock
__all__ = ['SignalProxy']
class SignalProxy(QtCore.QObject):
"""Object which collects rapid-fire signals and condenses them
into a single signal or a rate-limited stream of signals.
Used, for example, to prevent a SpinBox from generating multiple
signals when the mouse wheel is rolled over it.
Emits sigDelayed after input signals have stopped for a certain period of time.
Emits sigDelayed after input signals have stopped for a certain period of
time.
"""
sigDelayed = QtCore.Signal(object)
def __init__(self, signal, delay=0.3, rateLimit=0, slot=None):
"""Initialization arguments:
signal - a bound Signal or pyqtSignal instance
@ -26,32 +29,36 @@ class SignalProxy(QtCore.QObject):
rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a
steady rate while they are being received.
"""
QtCore.QObject.__init__(self)
signal.connect(self.signalReceived)
self.signal = signal
self.delay = delay
self.rateLimit = rateLimit
self.args = None
self.timer = ThreadsafeTimer.ThreadsafeTimer()
self.timer.timeout.connect(self.flush)
self.blockSignal = False
self.slot = weakref.ref(slot)
self.lastFlushTime = None
self.signal = signal
self.signal.connect(self.signalReceived)
if slot is not None:
self.blockSignal = False
self.sigDelayed.connect(slot)
self.slot = weakref.ref(slot)
else:
self.blockSignal = True
self.slot = None
def setDelay(self, delay):
self.delay = delay
def signalReceived(self, *args):
"""Received signal. Cancel previous timer and store args to be forwarded later."""
"""Received signal. Cancel previous timer and store args to be
forwarded later."""
if self.blockSignal:
return
self.args = args
if self.rateLimit == 0:
self.timer.stop()
self.timer.start((self.delay*1000)+1)
self.timer.start(int(self.delay * 1000) + 1)
else:
now = time()
if self.lastFlushTime is None:
@ -59,10 +66,10 @@ class SignalProxy(QtCore.QObject):
else:
lastFlush = self.lastFlushTime
leakTime = max(0, (lastFlush + (1.0 / self.rateLimit)) - now)
self.timer.stop()
self.timer.start((min(leakTime, self.delay)*1000)+1)
self.timer.start(int(min(leakTime, self.delay) * 1000) + 1)
def flush(self):
"""If there is a signal queued up, send it now."""
if self.args is None or self.blockSignal:
@ -70,10 +77,9 @@ class SignalProxy(QtCore.QObject):
args, self.args = self.args, None
self.timer.stop()
self.lastFlushTime = time()
#self.emit(self.signal, *self.args)
self.sigDelayed.emit(args)
return True
def disconnect(self):
self.blockSignal = True
try:
@ -81,31 +87,23 @@ class SignalProxy(QtCore.QObject):
except:
pass
try:
# XXX: This is a weakref, however segfaulting on PySide and
# Python 2. We come back later
self.sigDelayed.disconnect(self.slot)
except:
pass
finally:
self.slot = None
def connectSlot(self, slot):
"""Connect the `SignalProxy` to an external slot"""
assert self.slot is None, "Slot was already connected!"
self.slot = weakref.ref(slot)
self.sigDelayed.connect(slot)
self.blockSignal = False
def block(self):
"""Return a SignalBlocker that temporarily blocks input signals to this proxy.
"""Return a SignalBlocker that temporarily blocks input signals to
this proxy.
"""
return SignalBlock(self.signal, self.signalReceived)
if __name__ == '__main__':
from .Qt import QtGui
app = QtGui.QApplication([])
win = QtGui.QMainWindow()
spin = QtGui.QSpinBox()
win.setCentralWidget(spin)
win.show()
def fn(*args):
print("Raw signal:", args)
def fn2(*args):
print("Delayed signal:", args)
spin.valueChanged.connect(fn)
#proxy = proxyConnect(spin, QtCore.SIGNAL('valueChanged(int)'), fn)
proxy = SignalProxy(spin.valueChanged, delay=0.5, slot=fn2)

View File

@ -0,0 +1,153 @@
import sys
import pytest
from ..Qt import QtCore
from ..Qt import QtGui
from ..Qt import QT_LIB, PYSIDE
from ..SignalProxy import SignalProxy
class Sender(QtCore.QObject):
signalSend = QtCore.Signal()
def __init__(self, parent=None):
super(Sender, self).__init__(parent)
class Receiver(QtCore.QObject):
def __init__(self, parent=None):
super(Receiver, self).__init__(parent)
self.counter = 0
def slotReceive(self):
self.counter += 1
@pytest.fixture
def qapp():
app = QtGui.QApplication.instance()
if app is None:
app = QtGui.QApplication(sys.argv)
yield app
app.processEvents(QtCore.QEventLoop.AllEvents, 100)
app.deleteLater()
def test_signal_proxy_slot(qapp):
"""Test the normal work mode of SignalProxy with `signal` and `slot`"""
sender = Sender(parent=qapp)
receiver = Receiver(parent=qapp)
proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6,
slot=receiver.slotReceive)
assert proxy.blockSignal is False
assert proxy is not None
assert sender is not None
assert receiver is not None
sender.signalSend.emit()
proxy.flush()
qapp.processEvents(QtCore.QEventLoop.AllEvents, 10)
assert receiver.counter > 0
del proxy
del sender
del receiver
@pytest.mark.skipif(QT_LIB == PYSIDE and sys.version_info < (2, 8),
reason="Crashing on PySide and Python 2.7")
def test_signal_proxy_disconnect_slot(qapp):
"""Test the disconnect of SignalProxy with `signal` and `slot`"""
sender = Sender(parent=qapp)
receiver = Receiver(parent=qapp)
proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6,
slot=receiver.slotReceive)
assert proxy.blockSignal is False
assert proxy is not None
assert sender is not None
assert receiver is not None
assert proxy.slot is not None
proxy.disconnect()
assert proxy.slot is None
assert proxy.blockSignal is True
sender.signalSend.emit()
proxy.flush()
qapp.processEvents(QtCore.QEventLoop.AllEvents, 10)
assert receiver.counter == 0
del proxy
del sender
del receiver
def test_signal_proxy_no_slot_start(qapp):
"""Test the connect mode of SignalProxy without slot at start`"""
sender = Sender(parent=qapp)
receiver = Receiver(parent=qapp)
proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6)
assert proxy.blockSignal is True
assert proxy is not None
assert sender is not None
assert receiver is not None
sender.signalSend.emit()
proxy.flush()
qapp.processEvents(QtCore.QEventLoop.AllEvents, 10)
assert receiver.counter == 0
# Start a connect
proxy.connectSlot(receiver.slotReceive)
assert proxy.blockSignal is False
sender.signalSend.emit()
proxy.flush()
qapp.processEvents(QtCore.QEventLoop.AllEvents, 10)
assert receiver.counter > 0
# An additional connect should raise an AssertionError
with pytest.raises(AssertionError):
proxy.connectSlot(receiver.slotReceive)
del proxy
del sender
del receiver
def test_signal_proxy_slot_block(qapp):
"""Test the block mode of SignalProxy with `signal` and `slot`"""
sender = Sender(parent=qapp)
receiver = Receiver(parent=qapp)
proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6,
slot=receiver.slotReceive)
assert proxy.blockSignal is False
assert proxy is not None
assert sender is not None
assert receiver is not None
with proxy.block():
sender.signalSend.emit()
sender.signalSend.emit()
sender.signalSend.emit()
proxy.flush()
qapp.processEvents(QtCore.QEventLoop.AllEvents, 10)
assert receiver.counter == 0
sender.signalSend.emit()
proxy.flush()
qapp.processEvents(QtCore.QEventLoop.AllEvents, 10)
assert receiver.counter > 0
del proxy
del sender
del receiver