четверг, 18 мая 2017 г.

Qt Bug QAbstractItemModel beginMoveRows Ru

При попытке создания древовидной иерархической модели на базе QAbstractItemModel для QTreeView, и создания кнопки "переместить элемент вниз", столкнулся с неприятным багом в moveRows (точнее - beginMoveRows).

Я получил ошибку:

 ASSERT: "!this->isEmpty()" in file ..\..\include/QtCore/../../src/corelib/tools/qstack.h

Но ведь никакого QStack явно не используется!

При попытке выяснить причину,  оказалось, что программа падает на вызове endMoveRows, если sourceRow < destinationChild. (Реализация "переместить вверх" была тривиальна, но "вниз"... )
Нашлись следующие чем-то похожие баги:
https://bugreports.qt.io/browse/QTBUG-6940
https://bugreports.qt.io/browse/QTBUG-24337

Решение:
Если sourceRow < destinationChild уже нельзя использовать beginMoveRows, и поэтому решением было использовать сигналы layoutAboutToBeChanged и layoutChanged. (Я генерировал их, передавая индексы, однако без параметров тоже всё работает отлично.)
Так же добавил проверку beginMoveRows на истинность для других ситуаций.

Code fragment: 

bool TreeModel::moveRows (const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) {
        ...
         if(sourceRow > destinationChild) {
        // See https://bugreports.qt.io/browse/QTBUG-6940
        if(!beginMoveRows(sourceParent, sourceRow, sourceRow+count-1, destinationParent, destinationChild))
            return false;
        // Действия с данными здесь
        endMoveRows();
        return true;
        }
        // Because Qt bug :(. with endMoveRows, indexes and qstack.
        if(sourceRow < destinationChild) {
            QList<QPersistentModelIndex> parents;
            parents << QPersistentModelIndex(sourceParent) << QPersistentModelIndex(destinationParent);
            emit layoutAboutToBeChanged(parents);
            // Действия с данными здесь
            emit layoutChanged(parents);
            return true;
       }
...
}

Qt Bug QAbstractItemModel beginMoveRows En

When trying to create a tree-based hierarchical model based on QAbstractItemModel for QTreeView, and creating the "move item down" button (in one level), I encountered an unpleasant bug in moveRows (more precisely - beginMoveRows).

I got the error:

 ASSERT: "!this->isEmpty()" in file ..\..\include/QtCore/../../src/corelib/tools/qstack.h

But I know, that no QStack used by me!

When trying to find out the reason, it turned out that the program crashes on calling endMoveRows, if sourceRow < destinationChild. (The implementation of "move up" was trivial, but "down" ...)

I see next similar (but other models) messages:
https://bugreports.qt.io/browse/QTBUG-6940
https://bugreports.qt.io/browse/QTBUG-24337

My solution:
If sourceRow <destinationChild can no longer use beginMoveRows, and so the solution was to use the layoutAboutToBeChanged and layoutChanged signals. (I emited them by passing indexes, but without parameters, everything also works fine.)

I also added a beginMoveRows check to the truth for other situations.

Code fragment: 

bool TreeModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) {
        ...
         if(sourceRow > destinationChild) {
        // See https://bugreports.qt.io/browse/QTBUG-6940
        if(!beginMoveRows(sourceParent, sourceRow, sourceRow+count-1, destinationParent, destinationChild))
            return false;
        // Do somethig with items here
        endMoveRows();
        return true;
        }
        // Because Qt bug :(. with endMoveRows, indexes and qstack.
        if(sourceRow < destinationChild) {
            QList<QPersistentModelIndex> parents;
            parents << QPersistentModelIndex(sourceParent) << QPersistentModelIndex(destinationParent);
            emit layoutAboutToBeChanged(parents);
            // Do somethig with items here
            emit layoutChanged(parents);
            return true;
       }
...
}

It would be great if someone tried to reproduce and wrote to the official bugtracker Qt Framework
https://bugreports.qt.io/.

P.S.
Please, sorry my English. It's not my native language, and I use translator.

суббота, 13 мая 2017 г.

О PyQt5, наследовании от классов Qt и утечках памяти

Cделаю отступление, и напомню, что Qt изначально писался на С++ и для С++, будучи ориентированный под его специфику. (В С++ контроля и освобождения памяти, есть только некоторые приёмы обеспечивающие очистку памяти. В Python есть сборщик мусора, достаточно простой, но удалением объектов и освобождением занимается именно он. Так же доступ к объектам происходит по указателям - они в создаются в куче.)

глядя на код наподобии:

class MyDialog(QDialog):

    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.parent = parent
        self.initUI()

Тут мы можем получить ещё и кольцевые ссылки на уровне Python, но это другая история.
(К слову, если всё-таки требуется родительский элемент, стоит воспользоваться вызовом self.parent() .)

Да и просто во вроде бы безобидном классе:

class MyDialog(QDialog):
    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.initUI()

    def initUI(self):
        self.ok_button = QPushButton("OK", self)
        layout = QVBoxLayout(self)
        layout.addWidget(self.ok_button)
        self.show()

У меня были сомнения. Всё-таки стык между Python и С++. И на мой взгляд нужно быть хорошим знатоком одновременно Python, С++ и Qt, чтобы быть уверенным в происходящем.

Удивительно, но материалов в сети оказалось сравнительно мало:

http://stackoverflow.com/questions/37918012/pyqt-give-parent-when-creating-a-widget

Почти одно и то же, но на разных языках:
https://habrahabr.ru/post/210304/
http://enki-editor.org/2014/08/23/Pyqt_mem_mgmt.html

Проверить, остались ли в памяти виджеты (на сколько я понимаю, и С++ и Python сторон) можно,
добавив после вызова app.exec_() строку:
print('\n'.join(repr(w) for w in app.allWidgets()))

Один из способов избежать утечки памяти, переписать код так, чтобы ссылки хранились на стороне Python классов.

def openDialog(self):
    self.dialog = MyDialog()
    self.dialog.show()

Ещё один момент:

Если мы пишем свой класс, наследуясь от QWidget (или любых классов, унаследованных в свою очередь от QWidget), то безопаснее использовать атрибут WA_DeleteOnClose, если его можно будет установить.
Это обеспечит удалении класса на стороне С++, даже если обёртка на Python останется.
(В этом случае может возникнуть проблема, если остались не отсоединённые сигналы и слоты к С++ части класса.)

Для PyQt4:
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

Пример:

class MyDialog(QDialog):
    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)


Полный пример теста со stackoverflow, переписанный мной для PyQt5:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import (QApplication, QDialog,
                             QWidget, QPushButton, QVBoxLayout)
from PyQt5.QtCore import Qt


class MyDialog(QDialog):
    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.initUI()

    def initUI(self):
        self.ok_button = QPushButton("OK", self)
        layout = QVBoxLayout(self)
        layout.addWidget(self.ok_button)
        self.show()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    parent = QWidget()
    obj = MyDialog(parent)
    app.exec_()
    print('\n'.join(repr(w) for w in app.allWidgets()))


У меня показывает:
<PyQt5.QtWidgets.QDesktopWidget object at 0x000001F41CE94438>
<PyQt5.QtWidgets.QWidget object at 0x000001F41CE944C8>
<PyQt5.QtWidgets.QWidget object at 0x000001F41CE94288>

Что похоже на правду, поскольку parent и obj ещё в области видимости (scope) - она у них глобальная для скрипта (модуля).

Но obj остался только как обёртка - при попытке вызвать функцию из С++ сгенерировалась ошибка.

Если не писать self.setAttribute(Qt.WA_DeleteOnClose), то строк (а соответственно оставшихся в памяти объектов) будет больше, и они будут включать в себя QPushButton и всё остальное.

Несколько проверенных примеров просто с наследованием от QWidget или QMainWindow и созданием кнопок и прочих виджетов с указанием parent (а так же частичным хранением ссылок на них) показали утечку без WA_DeleteOnClose.

Итог:

Логичным выглядит стараться использовать self.setAttribute(Qt.WA_DeleteOnClose) в конструкторе при создании своих виджетов, главных окон и диалогов.
(В этом случае может возникнуть проблема, если остались неотсоединённые сигналы и слоты к С++ части класса.)

суббота, 14 января 2017 г.

bytes in doctest for Python 3.5


I wrote simple script and wont to use bytes in doctest.
No reason for use unittest or other solution, wich bigger than my script.

Simple way to wrote in doctest:
>>> foo(b'\xff\xd8\xff')
But problem, we have (caught) exception:
SyntaxError: bytes can only contain ASCII literal characters.

Yes, script file use UTF-8 encoding.

But we have simple solution: we can try write data as HEX:
>>> foo(bytes.fromhex('ff d8 ff'))

In result we write:
def foo(data):
    """
    >>> foo(bytes.fromhex('ff d8 ff'))
    """
    pass

Tested for Python 3.5.

For Python 2 i see, but not tested solution:
def foo(data):
    ur"""
    >>> foo(b'\xff\xd8\xff')
    """
    pass

No crossverion solution for 2.* and 3.* Python branches.

bytes в doctest для Python 3.5

Была необходимость использовать bytes в doctest функции, а ради одного теста небольшой функции смысла писать юнит-тесты никакого (скрипт достаточно маленький и простенький).

Большой соблазн написать в doctest:
>>> foo(b'\xff\xd8\xff')
Но мы получим exception:
SyntaxError: bytes can only contain ASCII literal characters.

Само собой файл скрипта в UTF-8.

Но у этого есть простое решение: записать данные в виде HEX:
>>> foo(bytes.fromhex('ff d8 ff'))

Получим:
def foo(data):
    """
    >>> foo(bytes.fromhex('ff d8 ff'))
    """
    pass

Проверено для Python 3.5.

Для Python 2. советуют писать следующим образом (не проверял):
def foo(data):
    ur"""
    >>> foo(b'\xff\xd8\xff')
    """
    pass

К сожалению нет решения одновременно для версий Python 2.* и 3.* .