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

Implement #152 #173

Merged
merged 19 commits into from
Jan 25, 2017
Merged

Implement #152 #173

merged 19 commits into from
Jan 25, 2017

Conversation

mottosso
Copy link
Owner

Hi all,

This is a first pass on #152, a strict enforcement of the PySide2 API.

It's a suggestion, so do not merge unless we've talked it through and are all happy with it.

Things to note:

  1. Qt.py is now twice as large (from 446 to 889 lines) as before, due to explicitly specifying each member.
  2. This will break existing code that uses members other than those found in PySide2.
  3. A number of inconsistencies were immediately found (and fixed) in tests, CAVEATS and examples. This is a good thing! Have a look.
  4. QtWebKit was excluded as it is not guaranteed to exist in PyQt4 and 5 (it's an optional add-on).

On (1), this isn't necessarily a disadvantage; it does make the code trivially simple and require close-to-zero understanding of the overall code to make contributions. Such as adding missing members, or members that become available (as PySide2 is still under active development).

Discussion

I personally think this is the right way forwards, but it does mean less code will run on it. At the moment, Qt.py is rather forgiving but then of course punishes you secretly further on as you attempt to run it on other bindings.

I could imagine we either make this leap, force users to conform to a strict subset of PySide2, or install a switch such as QT_STRICT=True.

$ python
>>> from Qt import QtGui
>>> widget = QtGui.QWidget()
>>> exit()
$ export QT_STRICT=True
$ python
>>> from Qt import QtGui
>>> widget = QtGui.QWidget()
AttributeError: 'module' object has no attribute 'QWidget'

If so, (1) is the added code worth the trouble? and (2) which value should be the default?

Let me know what you think.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Dec 20, 2016

I've written a quite large application using Qt.py which uses PySide2 when in Maya >= 2016.5, PySide when in Maya <= 2016, PySide when in Nuke and PyQt5 for all other scenarios (such as standalone use). This just works quite magically and well.

I need to give this a whirl before commenting further, but when using e.g. PyQt5 as the binding, and when hitting issues, what would be the solution at hand?
Should I query Qt.QtCompat.__binding__ and depending on what I'm doing I might have to import PyQt5 separately? (I hope not)

@mottosso
Copy link
Owner Author

This just works quite magically and well.

Did you mean this PR, or Qt.py in general?

when using e.g. PyQt5 as the binding, and when hitting issues, what would be the solution at hand?

What kind of issues?

@mottosso mottosso mentioned this pull request Dec 21, 2016
@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Dec 22, 2016

Did you mean this PR, or Qt.py in general?

Initially, I meant Qt.py in general. But after having now replaced Qt.py with the one from this PR I can just say I now also refer to this PR 👍

when using e.g. PyQt5 as the binding, and when hitting issues, what would be the solution at hand?

What kind of issues?

Sorry, I was being vague. When enforcing PySide2, I was merely wondering which approach to take when code break as a result of the enforcement. For example, I noticed a very large chunk of my code base (written for Qt.py 0.6.x) works fine but I got this error early on in Python 2.7 with PySide 1.2.4:

Traceback (most recent call last):
  ...
    ...
    quit_app.triggered.connect(QtWidgets.qApp.quit)
AttributeError: 'NoneType' object has no attribute 'quit'

Edit: the example error above is perhaps not the best kind of error to illustrate what my question is about... as I can just use QApplication instead of qApp and it'll play nice with all the bindings (PyQt4, PySide, PyQt5, PySide2) - and that's also what I should've done to begin with, I guess (even with Qt.py 0.6.x).

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Dec 22, 2016

I'm glancing through Qt.py top to bottom and when I reach the bottom half where all objects are mapped, I have a hard time understanding where the "_"-prefixed objects were created. For example:

Where in the code was _QtGui defined so that this mapping could be performed?:

QtGui.QAbstractTextDocumentLayout = _QtGui.QAbstractTextDocumentLayout

On how Qt.py now works
Also, let me see if I am getting this right;

  1. First the binding is imported and possibly remapped (e.g. QtWidgets = QtGui for PySide/PyQt4), and stored in a temporary "_"-namespaced module (e.g. _QtWidgets)
  2. We then build a QtWidgets fresh by manually specifying all its members and mapping them against the "_"-namespaced module _QtWidgets.

This all means that if a member is not part of the long list of mappings which now takes up approx. half of the Qt.py file, this won't end up in our custom-created QtWidgets. Am I correct?

Like you say, this makes the file bigger but it also becomes much easier to see exactly what is supported by Qt.py. I'd say if we make sure to explain every major section of Qt.py using inline comments and docstrings this would all contribute to improved readability of the file and make it easier for people to contribute.

Other questions

  • What about all the other modules which today aren't defined in Qt.py (e.g. QtQML, QtQuick); do we want to support them too?
  • How did you generate the long list of members, should perhaps a tool or guide (README or inline comment?) be included in the repository which can do this and aid in future development of Qt.py where new members needs to be added? Perhaps a test should be implemented which checks whether there are members of PySide2 which is not defined in Qt.py?

All in all, I like this PR and it seems all my code works great so far with it (although it was initially written for Qt.py 0.6.x and all I had to do was replace QtWidgets.qApp with QtWidgets.QApplication). Great job on this! 👍

Side note
I'm just curious; I see you define empty lists using some_list = list(). I usually just do some_list = [] from old habit. May I just ask if this is just a personal preference of yours or if there are any special advantages?

@mottosso
Copy link
Owner Author

Ok, now I understand.

This kind of enforcement should only cause one problem - a member is missing - for two causes.

  1. It does exist across all bindings, but hasn't yet been added
  2. It is missing from at least one binding, in which case it shouldn't exist

For (1), this layout would make it trivial to PR a new member, by simply adding a line to the long list of lines.

For (2), this is where enforcement kicks in. If something is missing and should be missing, that's a signal that it wouldn't work across all bindings.

With this PR, only members of PySide2 that also exists in all other bindings are included. It's a subset of PySide2, which does mean that Qt.py has the least amount of members out of all bindings.

qApp

As it happens, this is an interesting example. qApp is a special case that (I'm assuming) is added dynamically upon instantiation of QApplication. Before instantiation, the member does not exist.

Now the problem is that, qApp is only added to PySide2.QtWidgets, and not Qt.QtWidgets.

This is clearly a limitation of this approach.

What we could do is make Qt.QtWidgets into a class, rather than a module, and have it dynamically fetch items from the original QtWidgets.

class QtWidgets_(object):
  def __getitem__(self, attr):
    return getattr(_QtWidgets, attr)

But if we do that, we lose out on other module-like functionality, like this.

from Qt.QtWidgets import QPushButton

All in all, if we are to approach this issue of PySide2 enforcement with this particular technique - of explicitly specifying members - I'm not sure I see any other way than to bite the bullet on this one special case and accept that qApp is no longer viable, and that we should instead use QApplication directly (which, for those that do not know, is what qApp is an alias/shortcut to).

Worth considering!

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Dec 22, 2016

Okay, great - thanks for the clarification. So, overall, this clearly is an improvement as users are not tricked into writing cross-binding compatible code when they're not. Which is a important feature for my and my company's use of Qt.py; I write code which needs to work on multiple bindings.

So, whenever something doesn't work using Qt.py because of a member being exclusive to a specific binding, I guess we'll recommend you resort to doing something like:

from Qt import QtCompat

# Special case (member not supported by Qt.py)
if 'PyQt4' in QtCompat.__binding__:
    import PyQt4
    ...
elif 'PySide' in QtCompat.__binding__:
    import PySide
    ...
else:
    # Just use Qt.py...
    ...

...or would you advise differently?
It would actually be good to come up with a working concrete example where one might need to do this to explain this new (and backwards-breaking) functionality.

What we could do is make Qt.QtWidgets into a class, rather than a module, and have it dynamically fetch items from the original QtWidgets.

It would be interesting to get others thoughts on this. Personally, I think it would help maintain Qt.py (keeping it up to date automatically) by making it a class. Not being able to import like from Qt.QtWidgets import QPushButton is totally fine by me, however I appreciate this may or may not be the case with the majority of users (although I'm leaning towards the latter). The way it works right now makes for really nice readability though.

@mottosso
Copy link
Owner Author

Where in the code was _QtGui defined so that this mapping could be performed:

The three submodules QtGui, QtCore and QtWidgets are loaded within each function, like before. E.g. _pyside(). But this time, the function returns the loaded modules, and are later passed to here where they are given an underscore to separate them from the replacement submodules that we create.

For Qt 4, the _QtGui module is remapped to _QtWidgets for convenience in the module, neither are accessible one the module has finished loading, as you can see at the bottom of the module where they are explicitly deleted.

It means you can't do this.

import Qt
Qt._QtGui  # This would be the original binding.

This all means that if a member is not part of the long list of mappings which now takes up approx. half of the Qt.py file, this won't end up in our custom-created QtWidgets. Am I correct?

Precisely.

What about all the other modules which today aren't defined in Qt.py (e.g. QtQML, QtQuick); do we want to support them too?

Simple, they aren't supported by all bindings, therefore would not be included.

If you are looking for QtQML, odds are you aren't looking for compatibility with Qt 4, hence Qt.py is not what you are looking for.

There is however a case for PyQt5 and PySide2 compatibility, and it's possible we would still like to facilitate that. Otherwise, we could make that a separate project. Such as Qt5.py.

How did you generate the long list of members,

An excellent and important question.

The list was generated by the included build_membership.py module.

$ python build_membership.py

The resulting reference_members.json contains all members of PySide2, excluding those that do not exist in other bindings. Each of which are marked in-line.

Members are built given the currently installed version of PySide2, on Travis this means whatever version is available on apt-get. It's likely we'll need to keep up to date with that, but it's debatable whether we want to adhere to a newer version of PySide2 than what folks generally have access to, such as whichever version Maya happens to ship with. In that sense, it makes sense to go with the oldest version of PySide2, but that has other problems given it is still in Alpha.

Tough call!

In this PR, some members are doubly being excluded from within Qt.py as well, such as this one. Those should all ideally lie in a single spot for ease of maintenance.

some_list = list()

Just personal preference, I don't think there is any technical advantage.

@mottosso
Copy link
Owner Author

I guess we'll recommend you resort to doing something like:

I'm torn.

On one hand, if you find yourself needing to use non-cross-compatible Qt code, you might want to question why you use Qt.py.

On the other hand, there are always special cases. In some cases, one binding might be the recommended one due to some feature or bug in other bindings. In might be the case that a program runs best under PyQt5, and works less than ideal - perhaps with less features and glitter - under other bindings too.

Kind of like how Xbox One games sometimes works on Xbox 360, with lesser quality.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Dec 22, 2016

Regarding working around supporting a member not supported by Qt.py; I have no idea when and why I would need to do this but I wanted to raise the question to see how we feel about it.

There is however a case for PyQt5 and PySide2 compatibility, and it's possible we would still like to facilitate that. Otherwise, we could make that a separate project. Such as Qt5.py.

Yes, this what actually exactly which was on my mind. I think, at least for me, it's too early to start such a project. I'm head over heels in developing for practically all the bindings with Qt.py now and QML is just out of the question (because PySide/PyQt4). But I can clearly see the benefits of one such project (Qt5.py).

The list was generated by the included build_membership.py module.

Ah, nice. So basically you run this and copy-paste the members mapping from the JSON into Qt.py?
Sorry for all these questions... this PR is bigger than I have time reading today. I'll try reading this through more closely during the holidays. I'm glad and thankful that you're taking your time making clarifications :) Helps me be able to quickly grasp how this may affect developing Qt.py and developing with Qt.py.

The resulting reference_members.json contains all members of PySide2, excluding those that do not exist in other bindings. Each of which are marked in-line.

If these exclusions wasn't generated, how did you manage to list them (here)?
Did you just look these up manually and then added them to the exclusion lists?

Members are built given the currently installed version of PySide2, on Travis this means whatever version is available on apt-get. It's likely we'll need to keep up to date with that, but it's debatable whether we want to adhere to a newer version of PySide2 than what folks generally have access to, such as whichever version Maya happens to ship with. In that sense, it makes sense to go with the oldest version of PySide2, but that has other problems given it is still in Alpha.

A simple solution could be to just follow the vfxplatform spec and be observant of whether vendors do follow it or not and then adjust to what the industry standard actually ends up being.

@mottosso
Copy link
Owner Author

mottosso commented Dec 22, 2016

Did you just look these up manually and added them to the exclusion lists?

In the membership tests, after comparing members of PySide2 with each other binding, such as PyQt4, a listing of missing members is printed.

I then go in and manually add those to build_membership.py.

I figure it'll be a lot of work initially, but very little in the long run. I'd imagine we'll discover one or two members missing every now and then as time goes by and PySide2 is updated and that we'll simply add those to the list.

There might be more automatic ways of going at it, but I couldn't think of one that didn't make things too magical. Like the "hidden" reference_members.json currently produced by build_membership.py. Ideally I'd like those things to be out in the open so we can inspect and reference those in talks.

A simple solution could be to just follow the vfxplatform spec and be observant of whether vendors do follow it or not and then adjust to what the industry standard actually ends up being.

That is a great idea. Let's do exactly that.

I'm glad and thankful that you're taking your time making clarifications :)

My pleasure! A lot of these things I take for granted while typing it out. It sometimes doesn't occur to me that people can't read my mind until someone points it out, so do carry on!

@fredrikaverpil
Copy link
Collaborator

In the membership tests, after comparing members of PySide2 with each other binding, such as PyQt4, a listing of missing members is printed.

❤️

I was afraid you were doing that manually.

I figure it'll be a lot of work initially, but very little in the long run. I'd imagine we'll discover one or two members missing every now and then as time goes by and PySide2 is updated and that we'll simply add those to the list.

I must say, even if you think a lot is missing, my Python application (146.6 files/s, 17973.3 lines/s according to CLOC) just works with this 0.7.0 version of Qt.py.

Overall, a big thumbs up from me initially on this PR! 🥇

@mottosso
Copy link
Owner Author

Ok, I'm happy with this.

I've reduced the character-count significantly, and implemented QT_STRICT (which is off by default).

At the end of the day, this introduces a major shift in how Qt.py is implemented but should have minimal affect on users.

The most significant change is that Qt.py is no longer replaced by the original binding. This means members other than QtWidgets, QtCore, QtGui and QtCompat are no longer available.

Without QT_STRICT, this will still break code that previously worked on one binding but no others. For example, if someone was using QtTest or QtScript.

With QT_STRICT, my hope is that code should fail only when it would have failed on one or more bindings. The goal is to enable the user of Qt.py spot instantly when using something not entirely cross-compatible, and I think this is a good step in that direction.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Dec 22, 2016

This is all very excellent in my opinion 👍

The fact that you managed to get a QT_STRICT switch implemented is awesomesauce. Everybody wins.

To me, this also means we won't have to think much more about something like a "Qt5.py" project (discussed earlier) as I can easily use Qt.py (with QT_STRICT off) whenever I wish to use e.g. QML in a PyQt5/PySide2 combo situation.

I'm going to enable QT_STRICT for everything I do right now though. I really like this. I think QT_STRICT should become the default setting at some point. I'd go as far as to even raise the question whether maybe it should be enabled by default starting with 0.7.0. However, I do respect the fact that Qt.py is still young (although it has seen good adoption) and it might be hurtful to already create a legacy issue by enabling QT_STRICT by default.

With QT_STRICT, my hope is that code should fail only when it would have failed on one or more bindings. The goal is to enable the user of Qt.py spot instantly when using something not entirely cross-compatible, and I think this is a good step in that direction.

Haven't looked at your README changes just yet, but this should be prominent in the README and drive users towards wanting to enable QT_STRICT. I believe it should simply be a friendly recommendation which should have been enabled by default if we didn't already have users out there running code on Qt.py <= 0.6.x.

Great job!

@fredrikaverpil
Copy link
Collaborator

My compliments on getting the module inclusion lists (of _strict_members) much more readable as well.

@mottosso
Copy link
Owner Author

I'm going to enable QT_STRICT for everything I do right now though.

I'd encourage everyone to do the same. I'm expecting someone to try it and experience some issue that might prevent it to become the new default, but unless that happens I'd imagine it become the default once we hit 1.0.

this also means we won't have to think much more about something like a "Qt5.py" project (discussed earlier) as I can easily use Qt.py (with QT_STRICT off) whenever I wish to use e.g. QML in a PyQt5/PySide2 combo situation.

Actually that still wouldn't work, because only QtCore, QtGui and QtWidgets are available via Qt.py.

And also, what does "strict" mean if one is looking to use if for Qt 5 bindings only? I'd imagine Qt5.py to also have a strict mode, to exclude members exclusive to PySide2 or PyQt5.

I dunno, it doesn't feel right.

this should be prominent in the README

Agreed. I haven't found the right spot for it in the README. Too advanced to introduce it with, yet too important to leave for later. I was thinking of making it the central message of the Release.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Dec 22, 2016

Actually that still wouldn't work, because only QtCore, QtGui and QtWidgets are available via Qt.py.

Now I'm a bit confused. With QT_STRICT I thought we get this new functionality of the PR. With QT_STRICT disabled I thought we'd get the current 0.6.x behavior. No?

Sorry, I still haven't looked at the code in detail so I just assumed this.


When enabling `QT_STRICT`, Qt.py becomes a subset of PySide2. All members are guaranteed to exist across all bindings, meaning many will be missing. Including QtQml and QtQuick modules.

Strict mode follows the [VFX Platform](http://www.vfxplatform.com/) to determine which version of PySide2 to use for reference. Currently version if 2.0.0.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor typo in last sentence:

Currently version if 2.0.0.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thanks

@mottosso
Copy link
Owner Author

mottosso commented Dec 22, 2016

Now I'm a bit confused. With QT_STRICT I thought we get this new functionality of the PR. With QT_STRICT disabled I thought we'd get the current 0.6.x behavior. No?

Not quite, no.

See, before we simply made import Qt into whatever binding was chosen, for example import PySide2. Which would have exposed QtGui and QtWidgets like it does currently, but also all other members that are unique to PySide2 and PyQt5, such as QtQml.

The way this is implemented now, it Qt.py wouldn't replace itself with a module, it would be the actual module you use.

It's the only way we could have control over what's inside of it, and is probably how it should have been done to begin with. But it does mean things are different.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Jan 15, 2017

Sorry for the long idle time. Holidays, children and other private matters have been taking up my time.

I feel a bit thick, as I don't quite understand the exact definition of what will be the difference when having QT_STRICT enabled vs disabled.

I'm looking at this code from your original post:

$ python
>>> from Qt import QtGui
>>> widget = QtGui.QWidget()
>>> exit()
$ export QT_STRICT=True
$ python
>>> from Qt import QtGui
>>> widget = QtGui.QWidget()
AttributeError: 'module' object has no attribute 'QWidget'

Also looking in the code:

_strict() (QT_STRICT=True): This make Qt.py into a subset of PySide2 members that exist across all other bindings.
_loose() (QT_STRICT=False, default): This forwards attribute access to Qt.py submodules onto the original binding.

For _loose(), why exactly have you chosen to include only these?

  • QtWidgets
  • QtGui
  • QtCore
  • QtNetwork
  • QtHelp
  • QtXML

@mottosso
Copy link
Owner Author

QT_STRICT=False means you can still use QtCore.pyqtSignal, whereas QT_STRICT=True means you cannot. It means you can only use members that exist in PySide2, even if you are on another binding, such as PyQt4 or PyQt5. It means Qt.py would be a lowest common denominator of all bindings.

Having thought more about it over these past few weeks, I'm also open to the idea of not having a QT_STRICT=False mode at all. It would break backwards compatibility, but only in cases where code written with Qt.py would have broken on one or more bindings anyway. For example if one were to actually use pyqtSignal.

To ease the transition, we could increment the major release, making it 1.0.0, and let those that prefer the current more lax alternative stick to 0.x.x.

Writing new GUIs today, it makes the most sense to me. It would mean I know with confidence that if it works with Qt.py, it would work on all bindings.

What do you think?

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Jan 15, 2017

I totally agree with you; not having QT_STRICT=False.
I think it's a little bit confusing to have both modes. I can't really see when or why you'd want that.

I mean, with QT_STRICT=True you can always import <whatever binding> in case you need to do something out of the ordinary.

I feel having the 0.x.x behaviour (a.k.a. "wild west mode" 😉), is probably what people want if they don't want QT_STRICT=True, just like you say.

To ease the transition, we could increment the major release, making it 1.0.0, and let those that prefer the current more lax alternative stick to 0.x.x.

👍

0.x.x would probably be nice for those who are looking to port old code to Qt5. For new UIs, 1.0.0 would most likely be the way forward. This could be mentioned in the README, perhaps.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Jan 15, 2017

I don't know if it's worth doing, but we could maintain a branch which we call something like "legacy"... or something more clever. Which is in fact the 0.x behavior, retained.

EDIT: No, wait. Then travis testing etc won't work, I guess. Never mind.

@mottosso
Copy link
Owner Author

I totally agree with you; not having QT_STRICT=False.

Cool!

I can't really see when or why you'd want that.

It would be for backwards compatibility. For example, to those who have already built GUIs that work well with, e.g. PyQt5 and PyQt4. This update could break that.

EDIT: No, wait. Then travis testing etc won't work, I guess. Never mind.

We could instruct Travis to pick a particular branch, but whether we should maintain the previous version via a branch at all I'm unsure of.

What I'd like to do, is finish this up and make an alpha release. We make an announcement and ask for people's opinions. And move on from there.

One of the things we lose out on with this release is the immediate benefit to those who transition to PyQt5. Those users won't necessarily care for the current limitations of a pre-alpha PySide2, but they would be limited by it nonetheless. I think @melevine is such a user, correct me if I'm wrong!

On the upside, PySide2 will inevitably catch up, at which point this would no longer be a downside. Unless we care for compatibility with this first version of Maya 2017, which comes bundled with the pre-alpha version.

Choices!

@fredrikaverpil
Copy link
Collaborator

What I'd like to do, is finish this up and make an alpha release. We make an announcement and ask for people's opinions. And move on from there.

Sounds good to me! 👍

These are no longer relevant, as we do not modify the original module(s)
@mottosso
Copy link
Owner Author

Polished and QT_STRICT removed. Tested with GUIs within my possession, and it looks ALL OK to me.

Merging this tomorrow, but won't make an official release on PyPI until I've made an announcement and have had a few of you test this out for yourselves to find edgecases we'll need to take into account.

To install the pre-release, I'll be suggesting this.

$ pip install git+git://github.com/mottosso/Qt.py

Which is a method of installing Qt.py directly from GitHub, before releasing it into the wild.

And just to clarify, this will be a breaking release. Breaking things that would have broken on any of the four bindings, such as when using Qt.QtCore.pyqtSignal, but nothing else. On my GUIs, as I have been careful from the get-go, only one thing broke; using the QtWidgets.qApp macro, which is not part of this release.

@fredrikaverpil
Copy link
Collaborator

I'll be putting this to the test after lunch. Will be back with any impressions later today.

@mottosso
Copy link
Owner Author

mottosso commented Jan 25, 2017

Considering Qt.py no longer needs to replace itself with an original binding, we can add members to it without worrying about hiding those from the original.

Also considering this is already a breaking change, it'd be ideal to break as much as possible now, as opposed to spreading out breakage over several versions.

So I moved __version__, __binding__, __qt_version__ and __binding_version__ to the main Qt namespace.

For example.

# Before
>>> from Qt import QtCompat
>>> print(QtCompat.__version__)
u'0.6.9'

# Now
>>> import Qt
>>> print(Qt.__version__)
u'1.0.0.b1'

See this section of the README.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Jan 25, 2017

Considering Qt.py no longer needs to replace itself with an original binding, we can add members to it without worrying about hiding those from the original.

Excellent. Would it be useful to keep some backwards compatibility to Qt.QtCompat (e.g. keep these members also on QtCompat?
As we're hitting 1.0.0 I'd say that's not necessary.

@fredrikaverpil
Copy link
Collaborator

I just ran tests on internal stuff using Qt.py from this PR and it works fine without any hiccups.

@fredrikaverpil
Copy link
Collaborator

Would you like to still have the compatibility functions under QtCompat or do you wish to also move these onto Qt?

@mottosso
Copy link
Owner Author

Would you like to still have the compatibility functions under QtCompat or do you wish to also move these onto Qt?

As we still include compatibility wrapppers, like load_ui, I thought QtCompat was still relevant to those, and future wrappers that might appear. It's just these members, that didn't quite fit as "compatibility" related (at least to my mind).

But it's a detail, and I'd be happy to leave all additions to QtCompat if you'd prefer. What do you think?

@mottosso
Copy link
Owner Author

Here's what I've got in mind for an announcement on the VFX Platform.


Qt.py and safety

Hi community,

Today we've made a significant leap forwards for Qt.py and implemented a guarantee where if your program runs with Qt.py and any binding, such as PySide2, it will run in an identical fashion on any binding.

This means that it will throw an error if you use any feature of any particular binding that isn't available on another binding.

For example, this is currently possible with Qt.py

1: from Qt import QtGui, QtCore
2: 
3: class Widget(QtGui.QWidget):
4:   my_signal = QtCore.pyqtSignal(str)

On the third line, we run into an immediate problem; QWidget is only available via QtGui in PyQt4 and PySide(1).

On the 4th line, we refer to a signal as pyqtSignal which would work well on both PyQt4 and PyQt5, but fail in PySide and PySide2.

Worst yet, these problems would not make themselves known until you run it on each of the 4 bindings and make sure, first hand that it works as you'd expect.

With this new change, Qt.py limits the available members of each submodule into a subset of members that exist across all bindings. The result is finding out about these problems faster and being able to guarantee that if it runs on one binding, it works on all.

1: from Qt import QtGui
2: 
3: class Widget(QtGui.QWidget):
4:   pass
5:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'QWidget'

As a consequence of this breaking change, some of your already written software with Qt.py may need some tweaking. They would behave badly if they would have already behaved badly in any of the 4 bindings.

We are confident in this change and direction, but are still in discussion about whether now is the right time. One of the bindings, namely PySide2, is so far behind the other bindings that it is currently the "weakest link" by far.

These are the modules supported by Qt.py (and in effect, PySide2 (2.0.0)) with this change.

__all__ = [
    "QtGui",
    "QtCore",
    "QtWidgets",
    "QtNetwork",
    "QtXml",
    "QtHelp",
    "QtCompat"
]

The second concern regards which version of this weakest link to match. As PySide2 matures, more feature will be added and Qt.py could start to grow. But Maya 2017 and others are currently fixed at version 2.0.0. Growing Qt.py beyond what Maya 2017 is capable of would limit its usefulness.

For this I figure it wouldn't be too cumbersome to implement compatibility profiles; where the end-user specifies which version of, say, the VFX Platform to provide support for.

$ # For example
$ export QT_BASELINE=CY2018
$ python -c "from Qt import QtOpenGL"

For now, we'd like your feedback on this major change and for you to test it out for yourself. The release is still considered "alpha" and is available directly via GitHub for download or install.

$ pip install git+git://github.com/mottosso/Qt.py
  • Source
  • Pull request
  • Issue

Thanks to Matthew Levine for the initial feature request and Fredrik Averpil for the unwavering support. :)

Best,
Marcus

@fredrikaverpil
Copy link
Collaborator

But it's a detail, and I'd be happy to leave all additions to QtCompat if you'd prefer. What do you think?

Nah. I think this is good. :)

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented Jan 25, 2017

Here's what I've got in mind for an announcement on the VFX Platform.

The only addition I'd perhaps do is a simple example of how to detect which binding is being used and then do something binding-specific, such as:

from Qt import __binding__

if "PySide" in __binding__:
  do_pyside_stuff()

This should alleviate any headaches when reading... ;) Hehe. Joking aside, I think it can be good for clarity.

Very nice stuff! 👍

@mottosso
Copy link
Owner Author

Agreed and added.


For the time being, and where you need functionality exclusive to any particular binding, such as sip in PyQt, there is still the __binding__ member that allows conditional inclusion.

if "PySide" in __binding__:
  do_pyside_stuff()

This would allow you to explicitly mark the parts of your codebase that depend on any particular binding, and to enable a bridge to your code today and your code sometime in the future when the required functionality is officially part of Qt.py.

@mottosso
Copy link
Owner Author

Let's do it!

@mottosso mottosso merged commit ff812b7 into mottosso:master Jan 25, 2017
@mottosso mottosso deleted the implement152 branch January 25, 2017 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants