-
-
Notifications
You must be signed in to change notification settings - Fork 157
PR: Improve import modularity between QtGui
, QtWidgets
and QtOpenGL*
related modules
#387
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
PR: Improve import modularity between QtGui
, QtWidgets
and QtOpenGL*
related modules
#387
Conversation
Hi @DaelonSuzuka! The reason to do those imports is to bring compatibility with the Qt5 modules layout. For example, However, maybe we could somehow condition making these imports with a flag (something like |
QtGui
, QtWidgets
and QtOpenGL*
related modules
Good idea, I was thinking along those lines too. |
I guess that seems reasonable to me, a bit ugly but I'm not sure there's really another alternative, other than making it a lazy import or breaking backward compatibility. If we add a flag, we'd want it to documented in the README like the others. For backward compat, it should presumably by on by default in QtPy 2.x, though we could revisit that in QtPy 3.x. |
Thinking about it more, ISTM both the simplest and best approach here is try/except the MISSING_OPENGL_NAMES = {}
# ...
if PYQT6:
# ...
try:
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
except ModuleNotFoundError:
MISSING_OPENGL_NAMES.update("QOpenGLWidget")
# ...
def __getattr__(name):
if name in MISSING_OPENGL_NAMES:
raise QtModuleNotInstalledError(
name="PyQt6.QtOpenGLWidgets", missing_package="pyopengl")
else:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") That way, it would work gracefully right now without a hacky manual flag, while immediately raising a clearer and more specific/helpful error if it is not installed. The same approach could be applied to other optional modules as well, where they don't coincide exactly with a top-level QtPy (i.e. PyQt, PySide) submodule. Plus, it's shorter, simpler and can easily be re-used and extended to handle other missing optional modules. We could also use the same strategy (with a single |
Wow, I did not know that was an option.
That's a beautiful mechanism. I'll definitely try this out today. |
Yeah; see PEP 562 for more details.
👍 |
Co-authored-by: CAM Gerlach <[email protected]>
deb3a80
to
3c107df
Compare
Sorry for the conversation spam, I had to made some modifications to the mechanism and I wanted to explain the rationale.
The Real ProblemHow on earth do I write tests for this? I manually tested it in a project of mine by:
Something tells me there isn't a pytest helper for this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for implementing this, @DaelonSuzuka !
I didn't think about it before, but we need to set __cause__
on the error, so we can:
- Actually indicate that that it was an attribute access that was the proximate cause of the error, and inform the user which attribute required PyOpenGL
- Give the details of the ImportError/ModuleNotFound error that happened on import, giving them useful information about what actually happened rather than leaving them (and us or whomever they report it to) guessing whether the module is missing, there's a DLL issue, it was installed/uninstalled incorrectly, it was pip/conda mixing, etc.
Therefore, after a bunch of experimentation, since this added a lot of logic to every call site, I ended up putting this in a helper utility function and and calling that in __getattr__
.
I was originally going to make this as suggestions, but it turned out to be rather complicated, and prevent putting the common logic into a utility function somewhere else. Thus, I ended up just pushing it to a branch on my fork you can pull with
git remote add cam-gerlach https://github.com/CAM-Gerlach/qtpy.git
git fetch cam-gerlach
git pull cam-gerlach improve-import-hygiene
Or, I can push it to your branch directly, if you prefer. Thanks!
Seems like there's a merge conflict to solve, but we can deal with it after the current batch of changes (to avoid invalidating them).
NB, if you want to avoid others getting with with notifications for every comment, you can just do the comments as part of a review of your own PR, which will mark the as pending and then only send one notification once you submit it.
Ah, I see. I preferred your second, but I ended up refactoring it further and posted a patch/branch with it.
Our eventual goal is to have CI test environments with and without the optional deps, see #383 , which would allow testing this fully integrated, but for now to test this you should be able to patch |
Narrator: He did not, in fact, know how to do that. I'll have to try again after a sleep. |
Alright, I have to admit defeat: I cannot figure out how to test this. I thought I had it working for PySide6 with monkeypatching, but PyQt6 structures their imports differently and the same technique doesn't do anything there. It also only works if it's the first test that's run, which is obviously not correct. @CAM-Gerlach pls halp. |
Okay, sorry about that! While I previously thought it was possible, at least in some cases, as far as I can tell there's no obvious way to either get the Conda or the PyPI packages without OpenGL, as it is a core part of even the cut-down For starters, one thing that we should certainly add regardless here is add tests for the OpenGL classes in QtGui and QtWidgets, to actually verify that we're compatible here, and (that would presumably xfail if OpenGL was not present)—I assuming that's something you've done already locally. Beyond that, one thing we could do is just add a special matrix job where after installing but before running the test suite, we delete the OpenGL files from the installed package e.g. for file in Path(PySide6.__file__).parent.glob(*OpenGL*):
file.unlink() Then run the test suite, or a specific set of tests via a custom marker, checking an environment variable to xfail (with the correct error) or xpass the tests that should fail/pass based on the OpenGL install status. I'm not sure if that complexity (and an extra matrix job) is worth it for what seems to be a niche case; we have a plan to have separate jobs for scenarios with all the optional deps installed, and only the essentials, to test that things still work as expected, which would would presumably focus on what can be installed/uninstalled normally via pip/Conda, but would at least test the basic logic here. @dalthviz , what do you think here? |
This, I can definitely do.
This was the only idea I had left, but I couldn't tell if it was crazy or not. |
I think that should be enough 👍
As you said it seems complex and covers quite a niche case where basically you are removing things manually from the base bindings package, right? Maybe what could be worthy is to add some tests for the utility functions added and maybe add a test checking that the approach implemented with |
That's what it kinda seems like to me, yeah...
👍 |
I think the new tests are working correctly. Thank you @dalthviz for the recommendation. I manually tested this latest version on one of my work applications and I can strip |
Awesome! 🎉 I think we need to solve the conflict in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking pretty good so far; just some relatively minor suggestions. Thanks!
Co-authored-by: C.A.M. Gerlach <[email protected]>
That's the first merge conflict I've resolved using GitHub's editor, I'm crossing my fingers that I didn't mess it up. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, except for a problem with the merge resolution (see comment).
Co-authored-by: C.A.M. Gerlach <[email protected]>
Typical. I'm gonna blame github and their no-highlighting merge editor. Thanks for catching that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM from my end, thanks @DaelonSuzuka !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some small suggestions for you @DaelonSuzuka, otherwise looks good to me.
…score from _getattr_missing_optional_dep Co-authored-by: Carlos Cordoba <[email protected]>
I like both of those changes, thanks for suggesting them! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work here @DaelonSuzuka!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the work here @DaelonSuzuka !
For some reason,
QtGui
andQtWidgets
are importingQtOpenGL
andQtOpenGLWidgets
, respectively. Attempting to import these here forces applications to depend on OpenGL even if they don't need it.My use case for this change is that I distribute Qt/Python applications by creating a single-folder bundle with PyInstaller, and then zipping that bundle to create a portable distribution, or compiling it into an installer with a tool like InnoSetup. I was looking to reduce the size of my distributions, and Qt is supposed to be able to run with only a few of the dlls present, so I started deleting the biggest ones from my bundle.
opengl32sw.dll
is 20MB andQtOpenGL.pyd
is 10MB, so omitting them is a non-trivial reduction in final size.Am I missing a reason for these imports?
If removing these is okay, I can also audit the rest of the package for similar cross-contaminations (and I promise it won't spiral into a multi-month project like last time >.>).