Canonical Voices

Posts tagged with 'pyqt'

mandel

On Windows Ubuntu One uses the twisted reactor to run the Qt UI. The main reason for this is that the IPC protocol that is used was written in twisted. This has been giving us a number of head aches like the one we experienced today.

The following code simply shows a dialog that will as the user for his proxy credentials and will store them in the key-ring of which ever platform is being used:

def main():
    """Main method used to show the creds dialog."""
 
    if sys.platform == 'win32':
        import qt4reactor
        qt4reactor.install()
        logger.debug('Qt reactor installed.')
 
    app = QApplication(sys.argv)
    args = parse_args()
    win = ProxyCredsDialog(domain=args.domain,
                           retry=args.retry)
    return_code = win.exec_()
    if sys.platform == 'win32':
        from twisted.internet import reactor
        reactor.run()
    sys.exit(return_code)

From the dialog the most interesting part is the one in which the credentials are stored:

@defer.inlineCallbacks
def _on_save_clicked(self, *args):
    """Save the new credentials."""
    username = unicode(self.ui.username_entry.text()).encode('utf8')
    password = unicode(self.ui.password_entry.text()).encode('utf8')
    creds = dict(username=username, password=password)
    try:
        logger.debug('Save credentials as for domain %s.', self.domain)
        yield self.keyring.set_credentials(self.domain, creds)
    except Exception, e:
        logger.exception('Could not set credentials:')
        self.done(EXCEPTION_RAISED)
    logger.debug('Stored creds')
    self.done(USER_SUCCESS)

And to give even more details, the following is what is used to spawn a thread to store the credentials on windows:

def set_credentials(self, app_name, cred):
    """Set the credentials of the Ubuntu SSO item."""
    # the windows keyring can only store a pair username-password
    # so we store the data using ubuntu_sso as the user name. Then
    # the cred will be stored as the string representation of the dict.
    return deferToThread(self.keyring.set_password, app_name, USERNAME,
                             dumps(cred))

A priori there is nothing wrong with the code, or is it? Doing an IRL test you will see that the credentials are never stored, what’s even more none of the deferreds are called. But why? In theory the qt reactor should be taking care of everything which includes the deferreds, the deferToThread and the execution of the ui.. well, it is not. When we look a little closer we can see that we use the exec_ method from the QDialog and this is the root of the bug. Lets put an example, the following is possible in Qt:

import sys
 
from PyQt4.QtGui import QApplication, QDialog
 
app = QApplication(sys.argv)
dialog = QDialog()
dialog.exec_()

As you can see we are launching the dialog but we did not execute the application, but why is that? The main reason is found in the implementation of exec in the QDialog class (this time in C++, ouch!):

int QDialog::exec()
{
     Q_D(QDialog);
     if (d->eventLoop) {
         qWarning("QDialog::exec: Recursive call detected");
         return -1;
     }
 
     bool deleteOnClose = testAttribute(Qt::WA_DeleteOnClose);
     setAttribute(Qt::WA_DeleteOnClose, false);
 
     bool wasShowModal = testAttribute(Qt::WA_ShowModal);
     setAttribute(Qt::WA_ShowModal, true);
     setResult(0);
 
     show();
 
     QEventLoop eventLoop;
     d->eventLoop = &eventLoop;
     (void) eventLoop.exec();
     d->eventLoop = 0;
 
     setAttribute(Qt::WA_ShowModal, wasShowModal);
 
     int res = result();
     if (deleteOnClose)
         delete this;
     return res;
 }

As you can see the implementation uses a QEventLoop, if you read the documentation you will noticed that using exec of the event loops and the default flag all the events will me processed by this event loop, which is not the main event loop, but a child one (small brain fuck here). This means that, due to the implementation of the qtreactor, when using a dialog exec_ (or better say a QEventLoop.exec method) the reactor main loop is not processing the events which means that all your deferreds, deferToThread etc.. will not be processed :(

Nevertheless there is a way to work around this bug in the qtreactor implementation (because is not the qt fault and it is well documented) which is doing the following workaround:

win = ProxyCredsDialog(domain=args.domain, retry=args.retry)
win.show()
win.finished.connect(exit_app)

The above code ensures that the events will be handeled by the reactor main loop and not by a child QEventLoop.

In summary, qtreactor is buggy and will give you problems.. but if you really have to use it (like we do) do remember this detail, DO NOT use exec.

Read more
mandel

Recently in Ubuntu One we have been working or using PyQt for the UI of our application so that we could keep a smaller code base. While doing the move I noticed that we needed to have a widget similar to GtkArrow and to my surprise there is not one.

The following is a small implementation fo such a widget that might help other people that are in need of it. Even maybe someone that cares enough will write it in C++ and will propose it to Qt, sorry but I don’t have the time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 
from PyQt4.QtGui import QPainter, QStyle, QStyleOption, QWidget
 
 
class QArrow(QWidget):
    """Qt implementation similar to GtkArrow."""
 
   UP = 0
   DOWN = 1
   LEFT = 2
   RIGHT = 3
 
   def __init__(self, direction, parent=None):
       """Create a new instance."""
       QWidget.__init__(self, parent)
       if not direction in (QArrow.UP, QArrow.DOWN,
                 QArrow.LEFT, QArrow.RIGHT):
           raise ValueError('Wrong arrow direction.')
       self._direction = direction
 
    def paintEvent(self, event):
        """Paint a nice primitive arrow."""
        opt = QStyleOption()
        opt.initFrom(self)
        p = QPainter(self)
        if self._direction == QArrow.UP:
            primitive = QStyle.PE_IndicatorArrowUp
        elif self._direction == QArrow.DOWN:
            primitive = QStyle.PE_IndicatorArrowDown
        elif self._direction == QArrow.LEFT:
            primitive = QStyle.PE_IndicatorArrowLeft
        else:
            primitive = QStyle.PE_IndicatorArrowRight
        self.style().drawPrimitive(primitive, opt, p, self)

Took longer to think the correct way to do it than the actual coding, at the end is very simple.

Read more
mandel

So far using py2exe has not been walk in the park with several issues so far and this time it could not be different…. The interesting issue brought to me by py2exe this time was during the runtime of a pyqt application in which the following was being print to stderr:

QObject::moveToThread: Current thread (0x21a3410) is not the object's thread (0x19af0d0).

Funny enough that error would not happen if the application was not froze (WTF!). After some help from ralsina and some googling we found the following:

In my case I was pretty scared that the actual issue was related to the that the operation of the QObject was taking place in a twisted deferred callback and that qtreactor might be doing something naughty. At the end it turned out that the issue was related to the use of a jpg image which requires to use an image plugin in Qt… I fixed the issue as per this. Nevertheless I have made the required changes so that such a hack is hidden in the Windows code and that the Qt UI can be used in Kubuntu without dirty code so if everything goes as planned, SSO should have a buggy t UI that runs on Kubuntu.

Read more
mandel

The following bug I have been faced with has made me loose more time that I would have expected and therefore I think is a good idea to describe it so that the rest of the internet can take advantage of my wasted time and also to keep a record of my stupidity.

After building the .exe file of the ubuntu-sso port to windows I was getting the following error at runtime:

Cannot mix incompatible Qt library (version 0x40701) with this library (version 0x40702)

Usually this means that you have two different version of Qt installed are you are mixing the libraries (.dll in this case because I was dealing with Windows). My initial reaction was to look at my Qt setup in the machine and compared the version installed in the system and that used by PyQt. Let me tell you that is a waste of time. The real reason behind this runtime error was the fact that I was using the qtreactor and I had PyQt and PySide installed in my system.

When not freezing the application, the fact that you have both packages installed is not a problem what so ever, but with py2exe it is. Py2exe bundles all the dependencies you app has and due to the fact hat qtreactor does the following:

try:
    from PyQt4.QtCore import QSocketNotifier, QObject, SIGNAL, QTimer, QCoreApplication
    from PyQt4.QtCore import QEventLoop
except ImportError:
    from PySide.QtCore import QSocketNotifier, QObject, SIGNAL, QTimer, QCoreApplication
    from PySide.QtCore import QEventLoop

both, PySide and PyQt were included in the frozen app. The problem arises due to this fact. When py2exe adds both libs, it copies the Qt dlls you depend on, and if PySide and PYQt depend on different versions (which is what was happening in my system) you might run into the issue of getting dlls from different versions because py2exe will override the already copied dlls without telling you.

In summary if you get the above runtime error, take a look to see if PySide and PyQt have ben included in your frozen app and if they depend in different versions of Qt.

Read more
mandel

In some cases you might find yourself in the situation of wanting to use gettext in a PyQt project in which you have .ui files generated using QtDesigner.

For those kind of situations is a good idea to extend the uic compiler form PyQt. he following example shows how to do so in a distutils command.

class QtBuild(build_extra.build_extra):
    """Build PyQt (.ui) files and resources."""
 
    description = "build PyQt GUIs (.ui)."
 
    def compile_ui(self, ui_file, py_file=None):
        """Compile the .ui files to python modules."""
        # Search for pyuic4 in python bin dir, then in the $Path.
        if py_file is None:
            # go from the ui_file in the data folder to the
            # python file in the qt moodule
            py_file = os.path.split(ui_file)[1]
            py_file = os.path.splitext(py_file)[0] + '_ui.py'
            py_file = os.path.join('package', 'qt', py_file)
        # we indeed want to catch Exception, is ugle but w need it
        # pylint: disable=W0703
        try:
            # import the uic compiler from pyqt and generate the 
            # .py files something similar could be done with pyside
            # but that is left as an exercise for the reader.
            from PyQt4 import uic
            fp = open(py_file, 'w')
            uic.compileUi(ui_file, fp)
            fp.close()
            log.info('Compiled %s into %s', ui_file, py_file)
        except Exception, e:
            self.warn('Unable to compile user interface %s: %s',
                           py_file, e)
            if not os.path.exists(py_file) or\
                                            not file(py_file).read():
                raise SystemExit(1)
            return
        # pylint: enable=W0703
 
    def run(self):
        """Execute the command."""
        self._wrapuic()
        basepath = os.path.join('data',  'qt')
        for dirpath, _, filenames in os.walk(basepath):
            for filename in filenames:
                if filename.endswith('.ui'):
                    self.compile_ui(os.path.join(dirpath, filename))
 
    # pylint: disable=E1002
    _wrappeduic = False
    @classmethod
    def _wrapuic(cls):
        """Wrap uic to use gettext's _() in place of tr()"""
        if cls._wrappeduic:
            return
 
        from PyQt4.uic.Compiler import compiler, qtproxies, indenter
 
        # pylint: disable=C0103
        class _UICompiler(compiler.UICompiler):
            """Speciallized compiler for qt .ui files."""
            def createToplevelWidget(self, classname, widgetname):
                o = indenter.getIndenter()
                o.level = 0
                o.write('from module.with.gettext.setup import _')
                return super(_UICompiler, self).createToplevelWidget(
                                   classname, widgetname)
        compiler.UICompiler = _UICompiler
 
        class _i18n_string(qtproxies.i18n_string):
            """Provide a translated text."""
 
            def __str__(self):
                return "_('%s')" % self.string.encode(
                                                'string-escape')
 
        qtproxies.i18n_string = _i18n_string
 
        cls._wrappeduic = True
        # pylint: enable=C0103
    # pylint: enable=E1002

The above should be doable with PySide, but that is left as an exercise for the reader.

Read more