Skip to content
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

Using duck-typed signals (non-Qt) - can this cause problems? #94

Open
mkroutikov opened this issue Jul 14, 2020 · 1 comment
Open

Using duck-typed signals (non-Qt) - can this cause problems? #94

mkroutikov opened this issue Jul 14, 2020 · 1 comment

Comments

@mkroutikov
Copy link

mkroutikov commented Jul 14, 2020

Along with Qt Signal, I also use my own homegrown version which is api-compatible (connect/disconnect/emit), but does not use Qt code. Such signals are handy because some controllers can be used in a CLI-only applications (code reuse).

Not being very familiar with type hints, just getting a bit worried. My understanding is that runtime nothing bad can happen. But maybe UI (PyCharm?) or static type checkers will flag errors?

Question: do you see any problem with duck-typed non-Qt signals?

@altendky
Copy link
Owner

On the topic of signal-alikes I'll mention that I happened to sneak a little one of my own in here. I probably shouldn't have but it's not public API presently. :] It is a drop in descriptor layer that lets you put actual Qt signals on non-QObjects by creating the host QObjects automatically behind the scenes. There were just too many places in my application where I inherited from QObject exclusively to have signals.

qtrio/qtrio/_qt.py

Lines 10 to 53 in 0178ef6

class Signal:
"""This is a (nearly) drop-in replacement for QtCore.Signal. The useful difference
is that it does not require inheriting from `QtCore.QObject`. The not-quite part is
that it will be a bit more complicated to change thread affinity of the relevant
`QtCore.QObject`. If you need this, maybe just inherit.
This signal gets around the normally required inheritance by creating
`QtCore.QObject` instances behind the scenes to host the real signals. Just as
`QtCore.Signal` uses the Python descriptor protocol to intercept the attribute
access, so does this so it can 'redirect' to the signal on the other object.
"""
attribute_name = None
def __init__(self, *args, **kwargs):
class _SignalQObject(QtCore.QObject):
signal = QtCore.Signal(*args, **kwargs)
self.object_cls = _SignalQObject
def __get__(self, instance, owner):
if instance is None:
return self
o = self.object(instance=instance)
return o.signal
def object(self, instance):
d = getattr(instance, self.attribute_name, None)
if d is None:
d = {}
setattr(instance, self.attribute_name, d)
o = d.get(self.object_cls)
if o is None:
o = self.object_cls()
d[self.object_cls] = o
return o
Signal.attribute_name = qtrio._python.identifier_path(Signal)

But... sure, type hinting could make PyCharm complain. I think the solutions are 'interfaces' (like via zope.interface) or 'protocols' (typing and mypy) or maybe you can just tell PyCharm or mypy that your thing is close enough to a QtCore.SignalInstance?

I'm game for an exploration of supporting this case nicely though. Hopefully it really is just a type hinting thing. Hmm... I suppose if you tried to chain both Qt and non-Qt signals there could be trouble. But I don't recall anything offhand in QTrio itself that should care. I think the following are the trickiest things being done and should at worst require you implementing a couple things to be ducky enough.

qtrio/qtrio/_core.py

Lines 94 to 137 in 0178ef6

@attr.s(auto_attribs=True, frozen=True, slots=True, eq=False)
class Emission:
"""Stores the emission of a signal including the emitted arguments. Can be
compared against a signal instance to check the source. Do not construct this class
directly. Instead, instances will be received through a channel created by
:func:`qtrio.enter_emissions_channel`.
Note:
Each time you access a signal such as `a_qobject.some_signal` you get a
different signal instance object so the `signal` attribute generally will not
be the same object. A signal instance is a `QtCore.SignalInstance` in PySide2
or `QtCore.pyqtBoundSignal` in PyQt5.
Attributes:
signal: An instance of the original signal.
args: A tuple of the arguments emitted by the signal.
"""
signal: SignalInstance
args: typing.Tuple[typing.Any, ...]
def is_from(self, signal: SignalInstance) -> bool:
"""Check if this emission came from `signal`.
Args:
signal: The signal instance to check for being the source.
"""
# TODO: `repr()` here seems really bad.
if qtpy.PYQT5:
return self.signal.signal == signal.signal and repr(self.signal) == repr(
signal
)
elif qtpy.PYSIDE2:
# TODO: get this to work properly.
return bool(self.signal == signal)
raise qtrio.QTrioException() # pragma: no cover
def __eq__(self, other):
if type(other) != type(self):
return False
return self.is_from(signal=other.signal) and self.args == other.args

qtrio/qtrio/_qt.py

Lines 56 to 85 in 0178ef6

@contextlib.contextmanager
def connection(signal, slot):
"""Connect a signal and slot for the duration of the context manager.
Args:
signal: The signal to connect.
slot: The callable to connect the signal to.
"""
this_connection = signal.connect(slot)
import qtpy
if qtpy.PYSIDE2:
# PySide2 presently returns a bool rather than a QMetaObject.Connection
# https://bugreports.qt.io/browse/PYSIDE-1334
this_connection = slot
try:
yield this_connection
finally:
if qtpy.PYSIDE2:
expected_exception = RuntimeError
else:
expected_exception = TypeError
try:
# can we precheck and avoid the exception?
signal.disconnect(this_connection)
except expected_exception:
pass

I will note lastly here that you can use a QtCore.QCoreApplication for CLI apps afaik. But sure, I'm all for relegating Qt to doing GUI work so I appreciate your efforts on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants