enthought / pyface

pyface: traits-capable windowing framework
Other
106 stars 55 forks source link

Move `pyface.ui.qt4` to `pyface.ui.qt` #1223

Closed corranwebster closed 1 year ago

corranwebster commented 1 year ago

This moves pyface.ui.qt4.* to pyface.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 with qt4 in appropriate import statements.

But there are some circumstances where this may not be feasible:

This PR solves two of these problems by providing opt-in import hooks that replace imports of pyface.ui.qt4.* with corresponding imports of pyface.ui.qt.* so that sys.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:

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 import pyface.ui.qt4.foo; or looking at pyface version numbers). An alternative which will work in almost all cases is to instead use toolkit_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

mdickinson commented 1 year ago

And I believe neither create_module nor exec_module should be causing changes to sys.modules

Okay - that's nonsense, of course, since exec_module could be executing imports for other modules. It still seems odd to have create_module potentially altering sys.modules.

mdickinson commented 1 year ago

I'm not sure that this is right [...]

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.

mdickinson commented 1 year ago

Okay, back. I was falsely imagining that we'd be creating new modules for the qt4 imports that were shallow copies of the existing modules, but instead we're making those qt4 imports point to the existing qt modules (and caching in sys.modules).

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 __init__.py files, but we're not.

corranwebster commented 1 year ago

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.

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 pyface.ui.qt4 were shallow copies, but deeper ones were not. I might look at it briefly, but this is probably close enough for what we need as an optional, temporary thing.

corranwebster commented 1 year ago

This latest set of changes seems to resolve the issue with packages being duplicated: the solution was to tell the ModuleSpec that it is in fact a package by adding:

is_package=(new_spec.submodule_search_locations is not None),