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

Ignore the default empty layout objects when loading a .ui file #217

Merged
merged 38 commits into from
Dec 20, 2017

Conversation

tbttfox
Copy link
Contributor

@tbttfox tbttfox commented Jul 13, 2017

Lets try this again, this time without the phantom user:

The default empty layout object bool(QMainWindow().layout()) evaluates to True in PySide and False in PyQt4. So I added a check to see if an existing layout is empty.

@tbttfox
Copy link
Contributor Author

tbttfox commented Jul 13, 2017

Ok, I'm getting a Travis build failure on the PyQt implementations. From what I can track down, calling QMainWindowLayout::count() emits the warning qWarning("QMainWindowLayout::count: ?");, and that warning is what's causing the failure from inside uic.loadUi. However, I haven't the foggiest idea how to either fix it, or have the test ignore that warning.

@mottosso
Copy link
Owner

Thanks @tbttfox.

I've tracked down the original commit from which this clause got added, I think it'd be helpful to future readers if we included a comment on what it's doing there.

The reason being to mimic PyQt throwing an exception. Which now that I think of it sounds a little backwards. It's PyQt who's supposed to mimic PySide2.

Do you think it's possible to have PyQt support this feature, as opposed to PySide inheriting shortcomings of PyQt? It might not be, in which case this would be fine.

@tbttfox
Copy link
Contributor Author

tbttfox commented Jul 13, 2017

From what I can see, PyQt does support this feature. Nosepipe is just confused by the qwarning (that doesn't actually break anything), and I'm not familiar enough with nosepipe to tell it that the warning is expected, because it's not a normal python error.

And FYI:
My test passes all cases if I remove the layout existence check (the other tests fail, of course). So I'm guessing the act of checking for a layout on a new QMainWindow instance creates the layout [edit: did more testing, and I was totally wrong there]. PyQt4 then emits that qwarning when the ui is loaded.

@MHendricks
Copy link
Collaborator

From my testing for both PyQt and PySide:

  • QMainWindow().layout() returns a QLayout with a count of 0.
  • QDialog().layout() returns None.
  • QDockWidget().layout() returns a QLayout with a count of 2. It stores the close and float buttons used internally.

And if you call bool(widget.layout()) or if widget.layout():

  • QMainWindow returns False in PyQt, and True for PySide
  • QDialog returns False in both PyQt and PySide
  • QDockWidget returns True in both PyQt and PySide

This is consistent in both Qt4 and and Qt5 versions.

This means that due to the artificial check, you can't use Qt.QtCompat.loadUi on QDockWidget at all, and only in PySide/2 you cant use loadUI on QMainWindow.

I've done some testing and PyQt4/5 do not error if you use PyQt5.uic.loadUI on a Widget that already has a layout assigned to it. I used the code from the unit test to verify that. I only have access to PySide2 from inside Maya so I haven't verified this in PySide.

Qt's c++ code for QLayout raises a qWarning 'QLayout: Attempting to add QLayout "" to QMainWindow "", which already has a layout' I'm assuming this goes to stderr, and nose detects it as a error.

This is a c++ output, so I'm not sure if you can redirect its stderr output away from nose, but I think this is the original reason for raising the RuntimeError in Qt.QtCompat.loadUi. Just so the unittests didn't error out.

Here is a simple example of the problem, in this case it was run with PyQt4 from a windows command prompt:

>>> import sys
>>> from Qt import QtWidgets
>>> app = QtWidgets.QApplication(sys.argv)
>>> win = QtWidgets.QMainWindow()
>>> layout = QtWidgets.QVBoxLayout(win)
QLayout: Attempting to add QLayout "" to QMainWindow "", which already has a layout
>>>

No error was raised, but "QLayout: Attempting ..." is written to the command prompt, not python. I haven't seen it in Maya, but I assume its there just not being shown.

Is it possible to make nose ignore stderr from c++ for these specific tests? Or make Qt not log qWarnings? I don't think the check added in e4a736a is needed at all and it actually breaks things.

@MHendricks
Copy link
Collaborator

I did a little more searching and I think we could control the qWarning output using this function already exposed in Qt.py

http://doc.qt.io/qt-5/qtglobal.html#qInstallMessageHandler

Some example code that looks promising: https://stackoverflow.com/a/25681472

@mottosso
Copy link
Owner

Thanks for the excellent investigatory notes, @MHendricks.

I don't think the check added in e4a736a is needed at all and it actually breaks things.

Would you be able to take the lead on resolving that? I don't use loadUi myself so it's hard for me to find out about these kinds of problems.

@MHendricks
Copy link
Collaborator

It may be a few days before I can get to this (production support gets in the way). You don't need to test with loadUi, just attempt to create a layout and pass a parent widget(that already has a layout) into its constructor like I did in the code sample above.

I'll try to find some time to give it a try, or I can help @tbttfox debug it if he has time.

@CLAassistant
Copy link

CLAassistant commented Sep 12, 2017

CLA assistant check
All committers have signed the CLA.

tests.py Outdated


if binding("PyQt5") or binding("PySide2"):
def ignoreQtMessageHandlerFactory(msgs):

Choose a reason for hiding this comment

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

redefinition of unused 'ignoreQtMessageHandlerFactory' from line 155

tests.py Outdated


if binding("PyQt5") or binding("PySide2"):
def ignoreQtMessageHandlerFactory(msgs):

Choose a reason for hiding this comment

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

redefinition of unused 'ignoreQtMessageHandlerFactory' from line 155

tests.py Outdated
if msg in msgs:
return
sys.stderr.write("{0}\n".format(msg))
return ictxtMgr

Choose a reason for hiding this comment

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

indentation contains tabs

tests.py Outdated
msg = msg.decode()
if msg in msgs:
return
sys.stderr.write("{0}\n".format(msg))

Choose a reason for hiding this comment

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

indentation contains tabs

tests.py Outdated
if binding("PySide2"):
msg = msg.decode()
if msg in msgs:
return

Choose a reason for hiding this comment

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

indentation contains tabs

tests.py Outdated
msg = args[-1]
if binding("PySide2"):
msg = msg.decode()
if msg in msgs:

Choose a reason for hiding this comment

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

indentation contains tabs

tests.py Outdated
def ictxtMgr(*args):
msg = args[-1]
if binding("PySide2"):
msg = msg.decode()

Choose a reason for hiding this comment

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

indentation contains tabs

tests.py Outdated
def ignoreQtMessageHandlerFactory(msgs):
def ictxtMgr(*args):
msg = args[-1]
if binding("PySide2"):

Choose a reason for hiding this comment

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

indentation contains tabs

tests.py Outdated
@@ -122,6 +151,17 @@ def binding(binding):
return os.getenv("QT_PREFERRED_BINDING") == binding


def ignoreQtMessageHandlerFactory(msgs):
def ictxtMgr(*args):
msg = args[-1]

Choose a reason for hiding this comment

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

indentation contains tabs

tests.py Outdated
@@ -122,6 +151,17 @@ def binding(binding):
return os.getenv("QT_PREFERRED_BINDING") == binding


def ignoreQtMessageHandlerFactory(msgs):
def ictxtMgr(*args):

Choose a reason for hiding this comment

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

indentation contains tabs
indentation contains mixed spaces and tabs

@tbttfox
Copy link
Contributor Author

tbttfox commented Sep 13, 2017

Ok, so @MHendricks and I got together and got this working, but there's some extra stuff we found out

In the Qt4 bindings, the message handler is called qInstallMsgHander
In the Qt5 bindings, it's called qInstallMessageHandler
In both cases, they take exactly 1 function as an argument... however, that function requires a different signature between 4 and 5 (Two args in Qt4, vs. Three args in Qt5)

So, for now, we've added qInstallMessageHandler as a misplaced member, and it's up to the implementation to handle the different signatures. For the tests we just grab *args and parse from there.

There is probably a bigger discussion to be had on how to handle the message handlers, but I think we can do that in a separate PR.

tests.py Outdated
def test_load_ui_mainwindow():
"""Tests to see if the baseinstance loading loads main windows properly"""
import sys
from Qt import QtWidgets, QtCompat, QtCore

Choose a reason for hiding this comment

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

'Qt.QtCore' imported but unused

tests.py Outdated
msgs: list of message strings to ignore
"""
from Qt import QtCore
def messageOutputHandler(*args):

Choose a reason for hiding this comment

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

expected 1 blank line before a nested definition, found 0

@fredrikaverpil
Copy link
Collaborator

I'm sorry for the silence here. I'm very busy at work and my wife is expecting in about a week... so I'm not sure exactly when I'll be able to review this.

@tbttfox
Copy link
Contributor Author

tbttfox commented Dec 2, 2017

I pulled some commits from @MHendricks.
The layout existence check in _loadUi was removed. Having that check was causing failures on Linux where the code would run just fine otherwise. The test checking for this was removed.

Tests for running loadUi on QMainWindow QDialog and QDockWidget were added.

@MHendricks
Copy link
Collaborator

There is some inconsistency between the windows and Linux builds of Qt4 when dealing with QDockWidgets.

>>> from Qt.QtWidgets import QDockWidget
>>> dw = QDockWidget()
>>> dw.layout().count()
2
>>> dw.layout().isEmpty()
True

For QDockWidgets on Windows, isEmpty() returns True, but on Linux it returns False. I'm not sure why the layout existence check in _loadUi was added in the first place so I removed it.

QMainWindow has a default .layout() object and return False when .layout().isEmpty() is called on a new instance. This seems consistent for both Linux and Windows.

QDialog and QWidget do not have a default layout.

Also, thanks @tbttfox for cleaning up the unfinished mess I left on my test repo.

@mottosso
Copy link
Owner

mottosso commented Dec 4, 2017

For QDockWidgets on Windows, isEmpty() returns True, but on Linux it returns False.

Maybe we could add a QtCompat.isEmpty(my_widget) to account for the differences?

I'm not sure why the layout existence check in _loadUi was added in the first place so I removed it.

If you select a line here on GitHub and press the "Blame" button, it'll tell you the commit message for that line. In this case, I found:

Added exception to PySide loadUi if there is an existing layout to mimic PyQt4 behavior
reference

Not sure what this means given the changes in this PR. @dgovil do you remember what issues you where having before that commit? Does this resolve those issues?

QtCore.qInstallMessageHandler

Not sure how this slipped in here, there hasn't been any mention of this so far? I'm happy to keep it though, it looks well thought out and documented.

@tbttfox
Copy link
Contributor Author

tbttfox commented Dec 4, 2017

QtCore.qInstallMessageHandler

Not sure how this slipped in here, there hasn't been any mention of this so far? I'm happy to keep it though, it looks well thought out and documented.

It's been mentioned a couple times, but just for some explicit background: When checking for the non-existent layouts (in the check we removed), Qt would call a qWarning in some cases. This writes to the C++ stdout, which we couldn't seem to access from Python. So we had to use QtCore.qInstallMessageHandler to to either redirect, or silence that warning, otherwise NosePipe would get unexpected output, and fail.

Now that the check and the associated tests have been removed, it's not a strictly necessary part of this PR, but it's definitely worth having.

@dgovil
Copy link
Contributor

dgovil commented Dec 4, 2017

@mottosso It seems PyQt4 would error if you tried to load a ui file on to a widget that already had a layout assigned, which was the reason for that warning.

However I've recently discovered that it really depends on the PyQt version whether that is an issue or not. Some versions do not care while others do.

I'm not sure what's the best way to proceed here? IIRC the version that the tests download will fail, but the version we have at work does not. Not sure what's entirely different and noone seems to have answered me.

Maybe it's best to ignore them and if the test fails, acknowledge that it's expected behavior?

@tbttfox
Copy link
Contributor Author

tbttfox commented Dec 5, 2017

So I added a test to see that error. And it wasn't an actual error, it was just nosepipe freaking out about a qWarning again. I wrapped the test in a message handler, and everything passes!

@tbttfox
Copy link
Contributor Author

tbttfox commented Dec 5, 2017

After some thought, we decided that qInstallMessageHandler should be moved to QtCompat because of the handler signatures are different, and the types passed to the handlers are different.

And with that, I think this beast is finally ready, pending review.

@mottosso
Copy link
Owner

mottosso commented Dec 5, 2017

Well done everyone. I think it looks great. I've bumped the version and will merge and release once the tests do one final round.

@mottosso
Copy link
Owner

Merging this, sorry for the delay!

@mottosso mottosso merged commit 952b91d into mottosso:master Dec 20, 2017
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.

7 participants