-
Notifications
You must be signed in to change notification settings - Fork 55
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
Move pyface.ui.qt4
to pyface.ui.qt
#1223
Conversation
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.
We've already discussed the approach here, and I agree that this seems like a reasonable approach to the problem.
I'm not yet convinced that the Loader
subclass is implementing the Loader
interface in the way that the import machinery expects; but judging by the tests this is working in practice.
pyface/image/library/icons.zip
Outdated
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.
Changes to icons.zip
and std.zip
again; not sure if you want to keep these.
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.
Done.
@@ -19,7 +19,7 @@ | |||
from traits.etsconfig.api import ETSConfig | |||
|
|||
|
|||
USING_WX = ETSConfig.toolkit not in ["", "qt4"] | |||
USING_WX = ETSConfig.toolkit not in {"", "qt", "qt4"} |
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.
This is for a separate issue/PR since the issue predates this PR, but this seems a bit odd - we're implicitly assuming that if it's not Qt (and it's not null), then it must be wx.
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.
Opened #1224
pyface/ui/__init__.py
Outdated
from importlib.util import find_spec | ||
from importlib.machinery import ModuleSpec |
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.
Swap these around for 🔤 reasons? (Not sure how much we care. isort
would do something different here anyway, since it likes to put all the from
imports below the import
imports in any given section.)
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.
Done.
""" | ||
|
||
def __init__(self, fullname, new_name, new_spec): | ||
self.fullname = fullname |
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.
Could we add a docstring (either here, or at class level)? fullname
and new_name
are somewhat clear, but it's not clear to me without consulting external sources what the type or contents of new_spec
are expected to be.
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.
Done - I added to the class docstring.
|
||
def create_module(self, spec): | ||
if self.new_name not in sys.modules: | ||
import_module(self.new_name) |
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.
I'm not sure that this is right - from my reading of the docs, it should be exec_module
that's doing the work of actually populating the module, while create_module
should just be providing the unpopulated module object. And I believe neither create_module
nor exec_module
should be causing changes to sys.modules
- I think the import machinery takes care of that.
I'm not sure under what circumstances this would matter. I guess we lose the ability to reload
(since that would presumably be re-executing exec_module
but not create_module
), but that doesn't seem like a big loss. Having create_module
potentially alter sys.modules
worries me a bit more - I'm not sure what the ramifications of that could be.
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.
As you noted exec_module
has to be safe to make changes to sys.modules
because the code being executed might import things. Agree I'm not 100% sure about create_module
but I didn't see any admonitions against it and potentially a user implementation of the create_module
method could itself have lazy imports in it for things it needs. ie.
def create_module(self, spec):
import whatever_crazy_thing
...
But yes, I messed around with a number of different approaches, and settled on this one. The outcome that we want is that the module produced by this loader is
the module in sys.modules
for the corresponding pyface.ui.qt.*
name. This is straightforward if it's already imported, but if it isn't we need to create it somehow, and just using the standard import machinery seemed safer than trying to do it manually (and correctly) and then inject it into sys.modules
.
And unfortunately the current API helpfully takes care of adding things to sys.modules
for you (and adding the __spec__
) so the result of create_module
is I think the last place where we can change what is put into sys.modules
.
Reloading should work, since it will end up running the exec_module
of the original module that actually does the execution (but I don't care if it doesn't, I think).
self.traitsui_raise_patch = mock.patch( | ||
"traitsui.qt4.ui_base._StickyDialog.raise_" | ||
) | ||
try: |
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.
It feels as though we should just duplicate the TraitsUI helper in this repository to avoid the traitsui
imports. But not in this PR.
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.
Opened #1225
Okay - that's nonsense, of course, since |
Please ignore this. I was operating under the wrong mental model of how this was working. Let me go away and think and play a bit before I come back and embarrass myself further. |
Okay, back. I was falsely imagining that we'd be creating new modules for the In which case, this all seems to work. There does seem to be a minor oddity with packages (as opposed to modules), which as far as I can tell aren't shared, though I'm not sure why. (pyface) mdickinson@mirzakhani pyface % export ETS_TOOLKIT="qt4"
(pyface) mdickinson@mirzakhani pyface % python
Python 3.11.2 (main, Feb 10 2023, 08:20:03) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyface.ui.qt4.data_view as dv1
<stdin>:1: DeprecationWarning: The pyface.ui.qt4.* modules have moved to pyface.ui.qt.*
Backward compatibility import hooks have been automatically applied.
They will be removed in a future release of Pyface.
>>> import pyface.ui.qt.data_view as dv2
>>> dv1 is dv2 # expecting `True` here
False I guess this would be problematic if we were in the habit of putting classes or functions in our |
Yes, this is probably safe because of our coding practices; no, I have no idea why it's happening - for a time I had the problem that modules immediately under |
Co-authored-by: Mark Dickinson <mdickinson@enthought.com>
This latest set of changes seems to resolve the issue with packages being duplicated: the solution was to tell the
|
Recently a decision to move pyface.ui.qt4 to pyface.ui.qt has been made in pyface (enthought/pyface#1223) since versions newer than qt4 have become increasingly popular. However, this move can lead to import errors in other packages that are utilizing pyface when they try to import from qt4. For instance, in issue #1026 , we found that the example cannot find the enable.savage.trait_defs.ui.qt module since the default ETS_TOOLKIT is qt while the folder for qt is still enable.savage.trait_defs.ui.qt. The first way I proposed to solve this is by adapting the similar ShadowedModuleFinder approach in pyface (https://github.com/enthought/pyface/blob/main/pyface/ui/qt4/__init__.py). Now, Corran also proposed a simpler solution in the comment which is to use a simple check on import. This may be a simpler solution than using ShadowedModuleFinder. Closes issue #1026
This moves
pyface.ui.qt4.*
topyface.ui.qt
and helps with gradually deprecating all uses of "qt4". The current state is internal consistency, optional hooks for backwards compatibility for applications that depend on Pyface.Changes for downstream libraries and applications should be fairly simple: replacing
qt
withqt4
in appropriate import statements.But there are some circumstances where this may not be feasible:
pyface.ui.qt4.*
This PR solves two of these problems by providing opt-in import hooks that replace imports of
pyface.ui.qt4.*
with corresponding imports ofpyface.ui.qt.*
so thatsys.modules
points to the new location (avoiding issues with duplicated objects and modules which would happen with some other strategies). These hooks are not available by default, but can be accessed in a number of ways:ETS_QT4_IMPORTS
to directly turn on the import hooks, andETS_TOOLKIT=qt4
(as opposed toETS_TOOLKIT=qt
) with the assumption that this is an older environmentETSConfig.toolkit = "qt4"
(as opposed toETSConfig.toolkit = "qt"
) with the assumption that this is flagging that this is an application made with older assumptions about import locations; or if they need more control, they can install the import hooks intosys.meta_path
in a way that works best for them and any other import hooks that they might be using.The third case of a library developer who wants to support newer and older versions should be done through the usual mechanisms (eg. try importing
pyface.ui.qt.foo
and if it fails importpyface.ui.qt4.foo
; or looking at pyface version numbers). An alternative which will work in almost all cases is to instead usetoolkit_object()
to get the thing that they want, which will automatically get it from the right place.Done:
Note:
This ended up taking about a day longer than expected because the original working version of the code was using a deprecated API that would be removed in Python 3.12, so it needed re-implementation after digging in to understand what the new importlib API was doing. In doing so it has been generalized a bit, since we expect to also do this in TraitsUI at a minimum.
Fixes #560