Closed jacopoabramo closed 4 months ago
is it safe to use SignalInstance instead of Signal, when going with this approach? Do I risk making possible errors in connections or whatsoever?
yes, absolutely, if you don't want to use the descriptor, then you can create SignalInstances dynamically. The primary benefit of the descriptor is type checking, autocompletion, etc... (which I value as a developer). If you want to be fully dynamic, there's nothing stopping you from manually managing SignalInstance
however you want.
Is there any chance that SignalInterface could inherit from SignalGroup to maintain these traits and still be able to register SignalInstances dynamically?
yep, those objects are very similar.
on a quick glance, I'm not entirely following the motivation for the additional level of abstraction you're adding here; my quick (unstudied) feeling is that you're overcomplicating it a bit. It mostly sounds like you just need to manage a Mapping[str, SignalInstance]
. Where the key is the name of the signal and the value is a SignalInstance. This is what SignalGroups do, albeit, without dynamically adding additional signals after instantiation.
Is the point of all your dynamism that you just don't know what signals your plugins are going to offer? Or do you actually want them to be able to be mutable, and register additional signals even after they have been initially registered? Here is an example of a dynamically generated SignalGroup:
from psygnal import Signal, SignalGroup
DynamicGroup = type(
"DynamicGroup",
(SignalGroup,),
{"one_int": Signal(int), "int_str": Signal(int, str)},
)
my_signals = DynamicGroup()
my_signals.all.connect(lambda x: print("Emitting from all:", x))
my_signals.one_int.connect(lambda x: print("Emitting from one_int:", x))
my_signals.one_int.emit(1)
my_signals.int_str.emit(2, "two")
prints
Emitting from all: EmissionInfo(signal=<SignalInstance 'one_int' on <SignalGroup 'DynamicGroup' with 2 signals>>, args=(1,))
Emitting from one_int: 1
Emitting from all: EmissionInfo(signal=<SignalInstance 'int_str' on <SignalGroup 'DynamicGroup' with 2 signals>>, args=(2, 'two'))
or, in function form, similar to your registerSignals
:
def create_signals(sigs: Mapping[str, tuple[Any, ...]]) -> SignalGroup:
signals = {}
for name, types in sigs.items():
if not name.startswith("sig"):
raise ValueError("Signal name must start with 'sig' prefix.")
signals[name] = Signal(*types)
return type("DynamicGroup", (SignalGroup,), signals)()
Is the point of all your dynamism that you just don't know what signals your plugins are going to offer? Or do you actually want them to be able to be mutable, and register additional signals even after they have been initially registered? Here is an example of a dynamically generated SignalGroup:
The first, yes. The idea is that CommChannel should be a singleton object that can register different signals depending on the plugin. This happens only at startup at any rate. Once the signals are created, they're immutable.
Come to think of it I guess that using SignalGroup is not necessary in my application, I can just leave the situation as it is. Doesn't seem advantageous and the thing that matters the most to me is that using SignalInstance doesn't give problems.
I'll close this issue as well, thanks for the quick feedback.
Description
A project I'm working on uses an internal framework that inherits the behavior of some Qt base classes, specifically the most critical one is the Qt signals and slot mechanism. I wanted to move away from having to natively depend from Qt from some time now so I reimplemented most of these classes - including the signal - in native Python. In case of Qt signals I - of course - switched to psygnal. After understanding a bit better the descriptor protocol, I came to the conclusion that for my specific implementation I don't really want to use it natively. The reason is the following: my application will work in the future with plugins that should be able to dynamically register new signals to a common exchange point, and we cannot know in advance how many signals an exchange point will have; with the descriptor protocol this did not seem a viable approach because all signals need to be defined as attributes, just like we did in the original Qt implementation.
What I Did
I re-implemented the SignalInstance class as follows:
abstract.Signal
is just an abstract base class that provides the blueprint of how theSignal
class should look like, so it's just a bunch of@abstractmethods
. The re-implementation ofSignalInstance
is to make sure that I would implement correctly all abstract methods and properties provided by the blueprint.Together with this, I implemented a
SignalInterface
class which will be used to inherit from classes that want to emit signals (in origin,SignalInterface
would inherit fromQObject
; as I got rid of it, now the class is just an empty class that doesn't do much but needs to be kept for consistency with legacy code).Now bringing everything together, I have a class that inherits from
Signalinterface
and provides methods to dynamically register new signals as attributes:I made some not-so-deep testing of all this and it seems to be working according to the behavior I expected.
The questions I have are the following:
SignalInstance
instead ofSignal
, when going with this approach? Do I risk making possible errors in connections or whatsoever?SignalGroup
class which allows to bundle up together multiple signals and provide some nifty methods to extract them/connecting them in one go. Is there any chance thatSignalInterface
could inherit fromSignalGroup
to maintain these traits and still be able to registerSignalInstance
s dynamically?Let me know if you need some further clarification.