-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Type checking and auto-completion with PyQt6 (and PyQt5) #7370
Comments
We never supported PySide2 and by the time we get around to looking at any PySide version we'll likely be near to dropping Qt5 anyway. The main motivation however is avoiding, or being able to make use of, a quirk of pyright. When you import the same object from two different modules to the same attribute, and you don't have one of those modules installed, then pyright will tag the attribute with "Unknown". This affects us with the classes that got moved around between Qt5 and Qt6. Since we are importing them by name for both Qt5 backends then if you don't have one of them installed pyright will tag all those move classes as Unkown and you won't get autocompletion with them in VS Code. See #7370 for more details. mypy also has this issue (actually it's even worse there) but you can shortcut the ambiguouty by setting so called "compiled time constants" with the --always-true and --always-false command line arguments to make it not even consider some imports. pyright has a `defineConstant` setting for that too but it doesn't seem to apply to imports. I'll have to raise an issue with them...
I raised an issue with pyright here, lets see what they say about working in the same way as mypy: microsoft/pyright#4060 It seems that you can't tell it Edit: pyright has already pushed a release, 1.1.276, to let us use module attributes as constants. |
Define some constants for pyright to control how it handles the imports in qutebrowser.qt.* This is mainly for autocompletion and definitions with VS Code, which uses pyright via the pylance extension. If you have multiple possible places something could be imported from, and one of them isn't installed, the type of the thing being imported will fall back to Any, and you wont get nice things. So we use this file to make sure only certain imports are configured. The most important thing to remember about this file is it'll control where type definitions come from in VS Code. You can have multiple backends defined as true, generally the last import will win in that case. If any of the enabled imports aren't installed though you may not get any type hints at all. PyQt5 has been configured for now to match the type checking configured in CI. Personally I would be fine with PyQt6 configured here anyway since that's generally what we are developing against these days. See #7370 for more info.
I think this is partially done - the remaining part being getting |
Both |
This is split from #7202 and will probably make #7128 and the WIP V2 of that PR obsolete, as it looks like re-writing everything is in fact not necessary.
What I think the next steps on this workstream are:
Discussion of specific decisions, issues, subtasks etc of this issue should be done in a threaded fashion in the discussion: #7372
Following is a large amount of words on what we are trying to achieve, some backing on complications specific to out setup, simplified example, some background on prior art and solutions that don't work for us. The IDE part is still a bit of a mess.
Hopefully it's useful for people who haven't been across this stuff yet. Hopefully we can clean it up and refer back to it when we are working through the mypy issues.
What
Look at options to get type checkers and autocompletion working with conditional imports.
Why
With the move to support Qt6 we would like to introduce an abstraction mechanism to support multiple Qt wrappers for python. The major reasons are as follows:
Strategies for supporting multiple Qt bindings
There are a few strategies we have seen about the place for supporting multiple Qt bindings. One of the reasons we might choose one over the other is how easy or hard they are to get working with static type checkers. Generally we would prefer to go with the qts style strategy because that is the one that is the one that is already implemented on the qt6-v2 and having to go back and re-do stuff sucks.
expand for discussion of the different strategies with examples and pros/cons
qts/machinery style
Inspired by: https://github.com/python-qt-tools/qts/tree/main/src/qts
Uses wildcard import from Qt submodules into modules under our namespace
Includes some patching and renaming after import to present a consistent API across the backends
difficult for type checkers:
difficult for humans:
example:
file:
machinery.py
(same in all examples)file:
core.py
file:
application.py
rewrite style
Import modules into single file to avoid wildcard import
Have to change how we access Qt attributes since we can't do import from
Includes some patching and renaming after import to present a consistent API across the backends
difficult for type checkers:
difficult for humans:
example:
file:
machinery.py
(same in all examples)file:
qt.py
file:
application.py
generated
Calibre style: https://github.com/kovidgoyal/calibre/tree/master/src/qt
Similar to the machinery/qts style
Auto generated files of explicit imports and renames rather than wildcard import
Auto generated stub files for type checkers since actual imports are dynamic
Some strategies seen here could help mitigate slow startup with wildcard imports (eg autogenerate, or maintain, a big list of imports, instead of wildcard importing an even bigger list)
difficult for type checkers:
example:
file:
machinery.py
(same in all examples)file:
core.py
file:
application.py
(same as qts style)Conditional Imports
The main difficulty common to these strategies is the conditional imports. We have to hold mypy a bit carefully to help it reason about this situation.
some examples of attempts to help mypy reason about conditional imports without using the "compile time constants" command line arguments
Lets go over some examples of how mypy fails to deal with them and how we could help it.
This first one is a common enough pattern and works well enough at runtime. But not only is the second import explicitly called as re-using a variable name but mypy has no idea what to do with situation and so doesn't retain any type information for the
core
name.You can get around that "already defined" error by using a common variable like this. But mypy still doesn't know how to make sense of the situation, even within the branch where we do our best to remove ambiguity:
You'll see some guides on the internet saying to do something like
As a way to make the type checker avoid the ambiguity. Most of those cases are to deal with cases where you import the exact same module/object from different places. In our case PyQt5 and PyQt6 are totally different modules, which just happen to have largely overlapping APIs. It might be a way forward for us but it limits the amount of coverage we would get from type checking while we support multiple backends.
Here is a situation that shows how mypy compares the signatures between imports. We can see that it did pick a type for the imported object (the first one, I have both bindings installed in this case) but it also raises an error because the two branches import objects with different signatures:
There is a good write-up on options for this case here: https://stackoverflow.com/questions/59037244/mypy-incompatible-import-error-for-conditional-imports
What it doesn't mention is if we can choose to ignore that error in our binding abstraction layer. I don't think we can but we can come back to that if it becomes a problem with a more fleshed out strategy.
The current approach that qutebrowser uses is structured to make it easy to guide mypy using its command line args and then having a single place in the code (or a small set of variables that are consistently used) that guides which path we go down.
Consider these two examples files:
file:
machinery.py
file:
core.py
Just running mypy normally causes it to throw up a bunch of errors about incompatible imports being imported as the same name:
Since we are using centrally located variables to indicated which wrapper we are using we can use mypy's --always-true and --always-false arguments to cause it to pick certain paths.
You can also set these options in the configuration file like so:
file:
.mypy.ini
We may want to set our preferred bindings as the default in the config file to make basic mypy invocations more useful. (discussion: #7372 (comment))
Use Cases
There are a couple of different use cases we have for type hints.
type checking in CI
We would like to enable mypy CI jobs again. Options:
Given the above it makes sense to try for (2), try to run type checking for both bindings, one at a time.
It's also possible to just set it up for one binding for now and add the other one later. Although given we have much less errors with PyQt5 than PyQt6 it would probably be just PyQt5 in that case. And we don't won't to leave PyQt6 uncovered for long or it'll just introduce more drift.
There is some work to do on that front:
Any
and all the# ignore
comments about it are being complained about. But they are needed with PyQt5, not sure how to deal with that.IDE/Editor Autocompletion and Definitions
IDEs use type hints to provide autocompletion and find definitions for things. They have the same problem with multiple imports, they need to pick one import. We should hopefully be able to configure things to make the backend selection by IDEs stable. We may run into issues in the future managing installed backed, selected backends and mismatches between development environments and our CI setup.
VS Code is particularly interesting to support since it uses pylance for autocompletion which uses pyright as the backend.
Pyright is an alternative to mypy and some of the strategies above don't apply to it and it has different quirks.
The alternate LSP implementation that ships with VS Code, jedi, seems to have better autocompletion support in this case but it also appears to be significantly slower. There is apparently a mypy-lsp implementation but I have no idea how to get it working with VS Code.
For now we are shipping a pyrightconfig.json configuration file which supports
defineConstant
setting which is analogous to mypy's --allways-true/false settings (but better because it supports more than just bool). Like so:Rambling notes on how pyright deals with conditional imports *without* the defineConstant settings.
For example, here is our current
webenginecore
(with a couple of reveal_type()s added at the bottom):A bunch of modules got moved from webenginewidgets to webenginecore with the Qt6 release (they took the opportunity of a major version bump to clean up their codebase a bit) so we are importing some stuff from widgets in Qt5 to our wrapper so that non-wrapper code only has to know about them in one place.
Notice the difference between the two revealed types at the end.
QWebEngineNotification
is revealed asType[QWebEngineNotification]
butQWebEngineHistory
is revealed asType[QWebEngineHistory] | Unknown | Unbound
.Tellingly, we also do currently get autocompletion for
QWebEngineNotification
and we don't get it forQWebEngineHistory
.In this case I don't have either of the PySide modules installed. After installing both of them:
Suddenly there are no more possibly unbound variables and I also have autocompletion for QWebEngineHistory in VS Code!
So what is going on here? It seems the two failed imports (of PySide2 and PySide6) cause some possible interpretations of those variables to be tainted with
Unknown
orUnbound
which cause pylance not get any type information from them.Trying to narrow it down further it seems it needs either PySide2 OR PySide6 installed to get autocompletion for the moved modules. Why? Is it because I don't have type stubs for PyQt installed?
So it seems if you have "type checking mode" set it'll use a PySide library for type things.
BUT, if you go to webenginecore.py and remove both PySide imports...
How bizarre. I could understand if any of the binding modules being missing caused errors. Or just PySide2 since that (and PyQt5) are have some stuff imported from them by name, instead of wildcard. But why does just one of them (and it doesn't matter which one) make it work? Remember it only happens for the modules that got moved between Qt5 and 6, the difference between how we treat them is that we import those ones by name. So maybe it worries that the named variables would be overwritten by later wildcard imports? Yes indeed, commenting our all named imports from PySide2, but leaving in the wildcards, make it work. Interesting. More evidence that it treats the named import differently, if out of all four bindings you only have PyQt6 installed it works fine.
Does changing the order of the imports in the machinery file make it work as well? (I think it does lol.) But even that would be weird because with the last one uninstalled it can still be made to work.
It seems it selects the last imported definition and if an object is imported by name in any of them then then if any of the later imports may have overridden the name but the imports aren't available it'll give up. Uh, not quite, if pyside2 is installed but not pyside6 it works fine...
Okay I think it is like this:
So removing PySide2 "support" should fix this. BUT, then if you have PySide6 installed you would get autocompletion for that, and since we don't actually support it that is probably not so good.
The text was updated successfully, but these errors were encountered: