-
Notifications
You must be signed in to change notification settings - Fork 31
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
Support signal parameter/argument types #68
Comments
Allow variadic generics: python/typing#193 |
I really like this idea. However, I'm struggling to wrap my head around how we'll generify the difference between the stored signatures of a from PyQt5.QtCore import QObject, pyqtSignal
class Test(QObject):
valueChanged = pyqtSignal([dict, str], [list])
#### unbound. Note the repetitious indexing for the example
list_signal = Test.valueChanged[list][dict, str][list]
dict_str_signal = Test.valueChanged[list][dict, str]
# Will be the same. Both keep information about all signatures
assert list_signal.signatures == dict_str_signal.signatures
### bound
a = Test()
list_signal = a.valueChanged[list][dict, str][list]
dict_str_signal = a.valueChanged[list][dict, str]
# Allowed
a.valueChanged.emit({"b": "c"}, "test") # default signature
list_signal.emit([1, 2, 3]) # forced signature
dict_str_signal.emit({"b": "c"}, "test") # forced signature
# Not allowed.
a.valueChanged.emit([1, 2, 3]) # wrong usage of default signature
list_signal.emit({"b": "c"}, "test") # wrong usage of forced signature
dict_str_signal.emit([1, 2, 3]) # wrong usage of forced signature In the example above, the How can we make this work? |
Yeah, I've got no theories outside a plugin as to how you track the multiple overloaded signal signatures. But, I suspect that the majority of uses are with not-overloaded signals and even with them we could default to the... default overload. Maybe that's ok with some explanation of it and users can just I think I faked a hacky sorta-kinda-variadic solution for the not-overloaded-signatures part. https://mypy-play.net/?mypy=latest&python=3.8&gist=db80a939bf2224d5f6f95555c28d287a import typing
TA = typing.TypeVar("TA")
TB = typing.TypeVar("TB")
class pyqtBoundSignal(typing.Generic[TA, TB]):
signal: str = ""
@typing.overload
def emit(self) -> None: ...
@typing.overload
def emit(self, a: TA) -> None: ...
@typing.overload
def emit(self, a: TA, b: TB) -> None: ...
def emit(self, a: TA = ..., b: TB = ...) -> None: ...
class _ParameterNotAllowed: ...
class pyqtSignal(typing.Generic[TA, TB]):
@typing.overload
def __new__(self, *, name: str = ...) -> "pyqtSignal[_ParameterNotAllowed, _ParameterNotAllowed]": ...
@typing.overload
def __new__(self, a: typing.Type[TA], *, name: str = ...) -> "pyqtSignal[TA, _ParameterNotAllowed]": ...
@typing.overload
def __new__(self, a: typing.Type[TA], b: typing.Type[TB], *, name: str = ...) -> "pyqtSignal[TA, TB]": ...
def __new__(
self,
a: typing.Union[typing.Type[TA], typing.Type[_ParameterNotAllowed]] = _ParameterNotAllowed,
b: typing.Union[typing.Type[TB], typing.Type[_ParameterNotAllowed]] = _ParameterNotAllowed,
*,
name: str = ...,
) -> "pyqtSignal": ...
@typing.overload
def __get__(self, instance: None, owner: object) -> "pyqtSignal[TA, TB]": ...
@typing.overload
def __get__(self, instance: object, owner: object) -> pyqtBoundSignal[TA, TB]: ...
def __get__(self, instance: object, owner: object) -> typing.Union["pyqtSignal[TA, TB]", pyqtBoundSignal[TA, TB]]: ...
class D:
zero = pyqtSignal()
one = pyqtSignal(list)
two = pyqtSignal(int, str)
d = D()
# good
d.zero.emit()
d.one.emit([])
d.two.emit(1, "s")
# bad
d.zero.emit([])
d.zero.emit(1, "s")
d.one.emit("")
d.one.emit(1, "s")
d.two.emit(1, 2)
For the find-children stuff I was exploring I wrote up a silly little typevar/overload generator to fill part of the no-variadic-generics hole. It could be added to the source with some cog. (I keep saying I'll use cog but somehow I still haven't...) So we could then generate code to handle up to, say, 20 signal parameters with proper hinting. Maybe. https://repl.it/@altendky/CalmSickDistributeddatabase-1 import string
upper_letters = ['T' + c for c in string.ascii_uppercase]
def create_typevars(
names=upper_letters,
typevar='typing.TypeVar',
bound=None,
):
if bound is None:
bound_argument = ''
else:
bound_argument = f', bound={bound}'
return [
f'{name} = {typevar}("{name}"{bound_argument})'
for name in names
]
def main():
names = upper_letters[:4]
print('\n'.join(create_typevars(names=names, bound='QObject')))
template = 'def findChildren(self, types: typing.Tuple[{0}], name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[typing.Union[{0}]]: ...'
for index in range(len(names)):
print(template.format(', '.join(names[:index + 1])))
main() TA = typing.TypeVar("TA", bound=QObject)
TB = typing.TypeVar("TB", bound=QObject)
TC = typing.TypeVar("TC", bound=QObject)
TD = typing.TypeVar("TD", bound=QObject)
def findChildren(self, types: typing.Tuple[TA], name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[typing.Union[TA]]: ...
def findChildren(self, types: typing.Tuple[TA, TB], name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[typing.Union[TA, TB]]: ...
def findChildren(self, types: typing.Tuple[TA, TB, TC], name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[typing.Union[TA, TB, TC]]: ...
def findChildren(self, types: typing.Tuple[TA, TB, TC, TD], name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[typing.Union[TA, TB, TC, TD]]: ... |
Don't forget that These two should be equivalent. a = pyqtSignal([int, str])
b = pyqtSignal(int, str) |
Quite so, thanks for pointing that out. It was suggested that perhaps instead of a plugin we just write a wrapper of sorts that people could use instead when they want detailed hints. Also, a mypy plugin would only work with things that use mypy so it's not really an end-all solution anyways. So, the cost of the above partial solution (aside from being a hack) is that it would get in the way of the not-handled cases and make them extra ugly. Perhaps that's sufficient to rule it out. Maybe I can work out what a wrapper that is more Python and typing friendly would look like. |
That's a generate-classes-for-each-signal approach to getting overloads and connect/disconnect/emit signatures that aren't totally generic. Not something I expect us to implement here, but linking for completeness. (direct link to mypy-play example: https://mypy-play.net/?mypy=latest&python=3.9&gist=95795bc6793a6d4efa0ac061c5d93701) |
@altendky I noticed you referenced python/typing#193 on variadic generics in this thread. Heads up that we've been working on a draft of a PEP for this in PEP 646. If this is something you still care about, take a read and let us know any feedback in this thread in typing-sig. Thanks! |
@mrahtz, thanks for the heads up. I read through it and it does seem like it would help out with one aspect of the effort here. Signals support all sorts of further stuff like multiple signatures on one signal, connecting to slots which accept only some of the arguments, and I think more. I understand you may not have time to look over every attempt people make to apply the PEP, but if you are interested, see the second example below. For everyone else, here's an update to the non-variadic example including the various changes to the signal classes etc. https://mypy-play.net/?mypy=latest&python=3.8&flags=strict&gist=8ca3a353d2c1c5c100c1eb2d7db6ae79 import typing
TA = typing.TypeVar("TA")
TB = typing.TypeVar("TB")
class pyqtBoundSignal(typing.Generic[TA, TB]):
signal: str = ""
def __getitem__(self, key: object) -> "pyqtBoundSignal[TA, TB]": ...
def emit(self, a: TA, b: TB) -> None: ...
def connect(self, slot: "PYQT_SLOT") -> "QMetaObject.Connection": ...
@typing.overload
def disconnect(self) -> None: ...
@typing.overload
def disconnect(self, slot: typing.Union["PYQT_SLOT", "QMetaObject.Connection"]) -> None: ...
def disconnect(self, slot: typing.Union["PYQT_SLOT", "QMetaObject.Connection"] = ...) -> None:
# mypy-play doesn't seem to offer a 'stub' option
pass
class pyqtSignal(typing.Generic[TA, TB]):
signatures: typing.Tuple[str, ...] = ('',)
def __init__(self, a: typing.Type[TA], b: typing.Type[TB], *, name: str = ...) -> None: ...
@typing.overload
def __get__(self, instance: None, owner: typing.Type["QObject"]) -> "pyqtSignal[TA, TB]": ...
@typing.overload
def __get__(self, instance: "QObject", owner: typing.Type["QObject"]) -> pyqtBoundSignal[TA, TB]: ...
def __get__(self, instance: typing.Optional["QObject"], owner: typing.Type["QObject"]) -> typing.Union["pyqtSignal[TA, TB]", pyqtBoundSignal[TA, TB]]:
# mypy-play doesn't seem to offer a 'stub' option
pass
# Convenient type aliases.
PYQT_SLOT = typing.Union[typing.Callable[..., object], pyqtBoundSignal]
class QObject:
pass
class QMetaObject:
class Connection:
pass
class D(QObject):
signal = pyqtSignal(int, str)
reveal_type(D.signal)
d = D()
reveal_type(d.signal)
d.signal.emit(1, "s")
d.signal.emit(1, 2) And also a try at using the variadic feature described in PEP 646. I didn't see a Mypy PR for this so I didn't make any attempt to actually test it. import typing
Ts = typing.TypeVarTuple("Ts")
class pyqtBoundSignal(typing.Generic[typing.Unpack[Ts]]):
signal: str = ""
def __getitem__(self, key: object) -> "pyqtBoundSignal[typing.Unpack[Ts]]":
...
def emit(self, *args: typing.Unpack[Ts]) -> None:
...
def connect(
self,
slot: typing.Union[
typing.Callable[[typing.Unpack[Ts]], object],
pyqtBoundSignal[typing.Unpack[Ts]],
],
) -> "QMetaObject.Connection":
...
@typing.overload
def disconnect(self) -> None:
...
@typing.overload
def disconnect(
self, slot: typing.Union["PYQT_SLOT", "QMetaObject.Connection"]
) -> None:
...
def disconnect(
self, slot: typing.Union["PYQT_SLOT", "QMetaObject.Connection"] = ...
) -> None:
# mypy-play doesn't seem to offer a 'stub' option
pass
class pyqtSignal(typing.Generic[typing.Unpack[Ts]]):
signatures: typing.Tuple[str, ...]
def __init__(self, *args: typing.Unpack[Ts], name: str = ...) -> None:
...
@typing.overload
def __get__(
self, instance: None, owner: typing.Type["QObject"]
) -> "pyqtSignal[typing.Unpack[Ts]]":
...
@typing.overload
def __get__(
self, instance: "QObject", owner: typing.Type["QObject"]
) -> pyqtBoundSignal[typing.Unpack[Ts]]:
...
def __get__(
self, instance: typing.Optional["QObject"], owner: typing.Type["QObject"]
) -> typing.Union[
"pyqtSignal[typing.Unpack[Ts]]", pyqtBoundSignal[typing.Unpack[Ts]]
]:
# mypy-play doesn't seem to offer a 'stub' option
pass
# Convenient type aliases.
PYQT_SLOT = typing.Union[
typing.Callable[[typing.Unpack[Ts]], object], pyqtBoundSignal[typing.Unpack[Ts]]
]
class QObject:
pass
class QMetaObject:
class Connection:
pass
class D(QObject):
signal = pyqtSignal(int, str)
def accept_str_int(x: str, y: int) -> None:
return None
def accept_str_str(x: str, y: int) -> None:
return None
reveal_type(D.signal)
d = D()
reveal_type(d.signal)
d.signal.emit(1, "s")
d.signal.connect(accept_str_int)
# above should be good, below should be bad
d.signal.emit(1, 2)
d.signal.connect(accept_str_str) |
I'm missing a |
Well, unless I missed it there isn't any reference to applying |
So I don't know that this pans out but here's a very preliminary proof of concept that my initial thoughts were wrong and this is at least vaguely possible without a mypy plugin. It doesn't deal with the variadic nature of signal parameters nor does it make any attempt at #9. Anyways, I just wrote it up out of curiosity so figured I'd share it here. Though it may still require a mypy plugin to actually get it working right. Oh yeah, also doesn't cover connecting to a proper 'slot'.
Based on the signal classes in #56.
https://mypy-play.net/?mypy=latest&python=3.8&flags=strict&gist=fb8771c87a891edcfa30d522ae588638
The text was updated successfully, but these errors were encountered: