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

fix pyqtSlot result parameter type and Callable generic #102

Merged
merged 10 commits into from
Oct 7, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* [#51](https://github.com/stlehmann/PyQt5-stubs/pull/51) adds `pyqtBoundSignal.signal` hinted as `str`

### Changed
* [#102](https://github.com/stlehmann/PyQt5-stubs/pull/102) fix `pyqtSlot` `result` parameter type to `typing.Type[object]`
altendky marked this conversation as resolved.
Show resolved Hide resolved
* [#92](https://github.com/stlehmann/PyQt5-stubs/pull/92) remove self from `qDefaultSurfaceFormat()` and `qIdForNode()`
* [#83](https://github.com/stlehmann/PyQt5-stubs/pull/83) fixes `sip.array` to be generic
* [#79](https://github.com/stlehmann/PyQt5-stubs/pull/79) fixes extra class layer in several modules
Expand Down
4 changes: 2 additions & 2 deletions PyQt5-stubs/QtCore.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9232,7 +9232,7 @@ QT_VERSION = ... # type: int
QT_VERSION_STR = ... # type: str


FuncT = typing.TypeVar('FuncT', bound=typing.Callable) # For a correct pyqtSlot annotation
FuncT = typing.TypeVar('FuncT', bound=typing.Callable[..., typing.Any]) # For a correct pyqtSlot annotation
BryceBeagle marked this conversation as resolved.
Show resolved Hide resolved


def qSetRealNumberPrecision(precision: int) -> QTextStreamManipulator: ...
Expand Down Expand Up @@ -9264,7 +9264,7 @@ def oct_(s: QTextStream) -> QTextStream: ...
def bin_(s: QTextStream) -> QTextStream: ...
def Q_RETURN_ARG(type: typing.Any) -> QGenericReturnArgument: ...
def Q_ARG(type: typing.Any, data: typing.Any) -> QGenericArgument: ...
def pyqtSlot(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Optional[str] = ...) -> typing.Callable[[FuncT], FuncT]: ...
def pyqtSlot(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Type[object] = ...) -> typing.Callable[[FuncT], FuncT]: ...
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you feel about the typing.Type[object]. I starting with Type because I was trying to relate it to the function return type but... I didn't get that working. Maybe this should just be object/Any?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could FuncT be made generic? I think this would work

T = TypeVar("T")
FuncT = typing.TypeVar('FuncT', bound=typing.Callable[..., T])
def pyqtSlot(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Type[T] = ...) -> typing.Callable[[FuncT[T]], FuncT[T]]: ...

However, looking at the docs:

result – the type of the result and may be a Python type object or a string that specifies a C++ type

Not sure how the C++ string would interact with this. Maybe

def pyqtSlot(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Union[typing.Type[T], str] = ...) -> typing.Callable[[FuncT[T]], FuncT[T]]: ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that types has the same comment in the docs about "may be a Python type object or a string", so maybe we can be more specific than typing.Any

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean "can't be more specific"? When I read this I thought you made a comment then backed away from it. I did make an effort to relate the result parameter to the return type of the typing.Callable parameter and return but failed. I could make another attempt for you to look at.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No those were two different comments. I was suggesting that we change result to use a generic. Then I noticed that types is documented in a similar fashion and remarked that we could maybe do better than its current typing.Any

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the types, they can be any types including user defined and they don't necessarily even inherit from type what with metatypes, right? (plus str) So the only thing I can think of is to try to relate it with the parameters of the wrapped callable, but mypy doesn't do that yet. There's some related PEP but I can't find it at the moment. Do you have something specific in mind here?

I'll try what you shared though it looks familiar.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you're probably right that types is as good as mypy currently allows

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's yours along with the existing and a Protocol attempt.

https://mypy-play.net/?mypy=latest&python=3.8&gist=314620351fccbbfd297833d345b40a6d

import typing


FuncT = typing.TypeVar('FuncT', bound=typing.Callable[..., typing.Any])

def pyqtSlot(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Any = ..., revision: int = ...) -> typing.Callable[[FuncT], FuncT]: ...


T = typing.TypeVar("T")
FuncT2 = typing.TypeVar('FuncT2', bound=typing.Callable[..., T])
def pyqtSlot2(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Type[T] = ...) -> typing.Callable[[FuncT2[T]], FuncT2[T]]: ...


T_covariant = typing.TypeVar("T_covariant", covariant=True)
class P(typing.Protocol, typing.Generic[T_covariant]):
    def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> T_covariant: ...


def pyqtSlot3(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Type[T_covariant] = ...) -> typing.Callable[[P[T_covariant]], P[T_covariant]]: ...


def f(s: str, i: int) -> float: ...


reveal_type(pyqtSlot(result=float)(f))
reveal_type(pyqtSlot2(result=float)(f))
reveal_type(pyqtSlot3(result=float)(f))
main.py:11: error: Type variable "FuncT2" used with arguments
main.py:25: note: Revealed type is 'def (s: builtins.str, i: builtins.int) -> builtins.float'
main.py:26: error: Value of type variable "FuncT2" of function cannot be "Callable[[str, int], float]"
main.py:26: note: Revealed type is 'def (s: builtins.str, i: builtins.int) -> builtins.float'
main.py:27: note: Revealed type is 'main.P[builtins.float*]'
main.py:27: error: Argument 1 has incompatible type "Callable[[str, int], float]"; expected "P[float]"
Found 3 errors in 1 file (checked 1 source file)

I don't think we get to make a generic typevar.

Copy link
Collaborator

@BryceBeagle BryceBeagle Oct 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we need to make FuncT a TypeVar?

import typing

S = typing.TypeVar("S")
T = typing.TypeVar("T")
FuncT = typing.Callable[..., S]

def pyqtSlot(*types: typing.Any, name: typing.Optional[str] = ..., result: typing.Type[T] = ...) -> typing.Callable[[FuncT[T]], FuncT[T]]: ...


def f(s: str, i: int) -> float: ...


reveal_type(pyqtSlot(result=float)(f))
main.py:13: note: Revealed type is 'def (*Any, **Any) -> builtins.float*'
def f(s: str, i: int) -> float: ...


reveal_type(pyqtSlot(result=bool)(f))
main.py:13: note: Revealed type is 'def (*Any, **Any) -> builtins.bool*'
main.py:13: error: Argument 1 has incompatible type "Callable[[str, int], float]"; expected "Callable[..., bool]"
Found 1 error in 1 file (checked 1 source file)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You win (I think), thanks.

BryceBeagle marked this conversation as resolved.
Show resolved Hide resolved
def QT_TRANSLATE_NOOP(a0: str, a1: str) -> str: ...
def QT_TR_NOOP_UTF8(a0: str) -> str: ...
def QT_TR_NOOP(a0: str) -> str: ...
Expand Down
9 changes: 7 additions & 2 deletions tests/pyqtslot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from PyQt5.QtCore import pyqtSlot

@pyqtSlot(str)
def func(s: str) -> int:
def func_none(s: str) -> None:
BryceBeagle marked this conversation as resolved.
Show resolved Hide resolved
return

@pyqtSlot(str, result=int)
def func_int(s: str) -> int:
return 42

func("test")
func_none("test")
x = func_int("test") # type: int