-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RecursionError with v0.10.0 #292
Comments
Hi @hanjinliu, sorry you've hit this error. The main change that occurred in #281 was to ensure that, when a signal is emitted, all registered callbacks are called before the next emission event. This means that if one of the callbacks also triggers an emit event of the same signal (as your One solution that should work for you in both cases is to not emit unnecessary events in your value.setter if the value hasn't actually changed: @value.setter
def value(self, val: int) -> None:
before, self._value = self._value, val
if val != before:
self.value_changed.emit(val) However, your comment prompted me to go back and check what Qt's default behavior is for calling callbacks. And I realized it does not do a breadth-first callback pattern. Emission of a signal immediately invokes all callbacks, even if it happens in the middle of another emit event. For example: from qtpy import QtCore
class Emitter(QtCore.QObject):
sig = QtCore.Signal(int)
def cb1(value: int) -> None:
print("calling cb1 with", value)
if value != 2:
emitter.sig.emit(2)
def cb2(value: int) -> None:
print("calling cb2 with", value)
app = QtCore.QCoreApplication([])
emitter = Emitter()
emitter.sig.connect(cb1)
emitter.sig.connect(cb2)
emitter.sig.emit(1) prints calling cb1 with 1
calling cb1 with 2
calling cb2 with 2
calling cb2 with 1 So, @Czaki, with #281, we actually changed the behavior of psygnal to be unlike Qt, and possibly be more surprising than the previous behavior. Can you weigh in on what prompted your PR in #281, and why you think we should be calling all callbacks first before entering the second emission round? |
i think (but am not sure) that the motivation came from some behavior you were observing with EventedSet in napari, is that correct? Is it possible that that specific issue could have been solved without changing the callback order of nested emission events? |
as a slightly deeper example: from qtpy import QtCore
class Emitter(QtCore.QObject):
sig = QtCore.Signal(int)
def cb1(value: int) -> None:
print("calling cb1 with", value)
if value == 1:
emitter.sig.emit(2)
def cb2(value: int) -> None:
print("calling cb2 with", value)
if value == 2:
emitter.sig.emit(3)
def cb3(value: int) -> None:
print("calling cb3 with", value)
app = QtCore.QCoreApplication([])
emitter = Emitter()
emitter.sig.connect(cb1)
emitter.sig.connect(cb2)
emitter.sig.connect(cb3)
emitter.sig.emit(1)
|
I could confirm that also for native objects the order is same as you spotted: from qtpy.QtWidgets import QApplication, QListWidget
app = QApplication([])
w = QListWidget()
w.addItems(["a", "b", "c", "d", "e"])
w.currentRowChanged.connect(lambda x: w.setCurrentRow(2))
w.currentRowChanged.connect(lambda x: print("currentRowChanged", x))
w.show()
w.setCurrentRow(1) gives:
The problem is that in such a situation you cannot depend on values emitted by signal, as the last signal contains different value than the current state in the object, that introduces inconsistency in the system. If you decide to revert this change, then there will be a need to make sender dispatch thread safe. |
Thanks @Czaki, your example indeed makes it very clear what the problem is with the default pattern. So, while I do think the default behavior should return to how it was, I think we need an option flag. There are three places where I can imagine having this (not mutually exclusive). # defined on the signal level
class Thing:
sig = Signal(int, depth_first=False)
t = Thing()
# declared at the connection level
t.sig.connect(print, depth_first=False)
# declared at emission time
t.sig.emit(1, depth_first=False) ... option 2 (a connection parameter) would perhaps be the weirdest one, with possibly confusing results and a more difficult implementation, but it would be the only one where the receiver gets to specify how they want to be called. There's many ways we could go with this, and I think it's a worthwhile feature/switch. Thoughts? |
There is also a need to allow passing such selection to evented model. Also qt is not consistent in order of event emmision from qtpy.QtWidgets import QApplication, QListWidget
from qtpy.QtCore import Qt
app = QApplication([])
w = QListWidget()
w.addItems(["a", "b", "c", "d", "e"])
w.currentRowChanged.connect(lambda x: w.setCurrentRow(2), Qt.QueuedConnection)
w.currentRowChanged.connect(lambda x: print("currentRowChanged", x), Qt.QueuedConnection)
w.show()
w.setCurrentRow(1)
app.exec_() provides
|
right, I'll put together a proposal |
hi @hanjinliu, I just pushed v0.10.1 which should fix the recursion error you're seeing in your original example. One can now opt-in to the new behavior with class Events(SignalGroup):
value = Signal(int, recursion_mode="deferred") |
Hi @tlambert03 , thank you for letting me know. |
Description
I found in a specific case, psygnal<0.10 works fine but recursion error occurs in the emission loop with 0.10.0.
What I Did
This is a simple example that class
A
has an evented propertyvalue
and classLinker
can link the values of differentA
instances.The text was updated successfully, but these errors were encountered: