четверг, 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.* .

вторник, 29 ноября 2016 г.

Sigil Image downloader plugin

К сожалению, вопрос написания плагинов для Sigil очень мало освещён. Нужно постараться, чтобы найти даже официальный мануал, содержащий скупые описания. И ни они, ни исходники не отвечают на некоторые интересные вопросы.

Полезные ссылки, все на английском:

https://github.com/Sigil-Ebook/Sigil/blob/master/docs/Sigil_Plugin_Framework_rev7.epub - официальный мануал.
http://www.mobileread.com/forums/attachment.php?attachmentid=140885&d=1439113322 - PDF версия с некоторыми примерами.
https://github.com/Sigil-Ebook/Sigil/blob/master/src/Resource_Files/plugin_launchers/python/bookcontainer.py - Один из самых важных заголовочных файлов
Заголовки https://github.com/Sigil-Ebook/Sigil/tree/master/src/Resource_Files/plugin_launchers/python

Результат:
Исходный код: https://github.com/yastrov/imagedownloader-sigil-plugin
Релиз: https://github.com/yastrov/imagedownloader-sigil-plugin/releases

Итак:
Периодически есть желание сделать копию статьи из интернета, и часто там присутствуют изображения - фотографии, картинки, иллюстрации.
Можно с одной стороны сохранять целиком веб-страничку. А можно написать плагин, и просто копировать нужный фрагмент статьи (или её всю целиком) в Sigil и просто запустить плагин. К счастью, авторы не стали изобретать что-то странное и взяли привычный и простой Python.

Интерпретатор версии 3.4 встроен в версию Sigil для Windows (поставляется вместе), и содержит много полезных библиотек, например PIL, html5lib, regex, lxml, библиотеки для работы с CSS и другие.
В версии для  Windows х64 библиотеки можно увидеть по адресу (по умолчанию): C:\Program Files\Sigil\python3\Lib\site-packages
Для х32 - C:\Program Files (x86)\Sigil\python3\Lib\site-packages
Но можно использовать и свой интерпретатор, указав его в настройках "Управление модулями" Sigil (установка плагинов происходит через неё же, почитать про установку плагинов можно в мануале или здесь: http://www.rshelton.org/2015/10/new-version-of-sigil.html).

Чтобы плагин был плагином, нужно создать plugin.xml с нехитрым содержимым (Его можно будет посмотреть или в мануалах, или в репозитории на GitHub). Единственная особенность: не стоит пытаться указывать совместимость с python3.5 - плагин просто не загрузится.
Только с python3.4 или python2.7 (Рекомендован всё же 3.4).

Сам скрипт, главная часть плагина - имеет имя plugin.py.

Входной точкой в плагин будет функция run, принимающая объект класса BookContainer с именем bk и возвращающая код завершения (успеха) - 0:

def run(bk):
    pass
    # наши действия будем писать здесь.
    return 0

Все остальные функции либо будут вызываться из run, либо написаны "на всякий случай, если скрипт запустят из терминала, без загрузки в Sigil".

Чтобы обойти все файлы с текстом (html, а точнее xhtml странички), мы воспользуемся, как и советуют, следющей конструкцией:

for (_id, href) in bk.text_iter():
    html = bk.readfile(_id)
    if not isinstance(html, text_type):
        html = str(html, 'utf-8')
    html_orig = html
    # наши действия
    if not html == html_orig:  # Проверяем, что содержимое файла наш скрипт изменил
        bk.writefile(_id, html)

С помощью bk.text_iter() обойдём все странички с текстом книги, получая для каждой _id - идентификатор (по факту - имя файла в стиле Section0001.xhtml), и адрес (например: ../Text/Section0001.xhtml). _id Мы потом используем для сохрания текста с помощью bk.writefile(_id, html).

Чтобы выяснить, какие файлы изображений уже есть в книге, используем bk.image_iter():
exists_image_id_list = list(_id for _id, _, _ in bk.image_iter())

Казалось бы. Но не всё так просто - поднимается вопрос "как сохранять наше изображение в книгу"?
Sigil API, а точнее bookcontainer.py предлагает функцию addfile класса BookContainer.
addfile требует загадочный параметр uniqueid, basename и data.
data - это содержимое нашего файла, бинарные данные.
basename - по логике имя файла без пути, т.е. скажем image.png
А uniqueid? Мануал молчит. Экспериментально, (а заодно подсмотрев в те id, что возвращает text_iter() ), было установлено - в этом качестве прекрасно подходит имя файла (то же самое basename).
И фактически (в исходниках используется uniqueid, но смысл тот же) получаем вызов:
bk.addfile(basename, basename, data)
Остальное Sigil сделает самостоятельно.

Проверить существование uniqueid можно либо попытавшись загрузить файл из книги bk.readfile,
либо вызвав bk.basename_to_id(uniqueid), которая вернёт либо None, если такого id ещё не встречалось, либо значение uniqueid, если уже существует и был использован.

Остальное - дело техники:
- Найти ссылки на странице с помощью простого регулярного выражения:
urls = re.findall(r'<img.+?src=["\'](http.+?)["\']', html, re.I + re.M)
- пройтись по каждой из них, получая адреса файлов банальным url[url.rfind('/') + 1:]
- загрузить файл с помощью штатного urllib.request.urlopen
* Для этого создана функция get_data(url), строка 16 скрипта *
* Да, действительно есть прекрасный requests, но это добавляет скрипту лишнюю зависимость. А так хоть и без поддержки сессий HTTP 2, но зато работает из коробки, в том числе на встроенном в Sigil интерпретаторе.*
- Проверить, есть ли у нас такой же файл
* Для проверки используется хэш сумма SHA512 так же из штатной библиотеки:
 hashlib.sha512(data).digest(), которая сравнивается с тем файлом, уже существующим в книге, у которого совпадает имя (оно же uniqueid) *
- Создать уникальное имя файла, и проверить его
* Для этого создана функция make_unique_id_for_img(unique_id, exists_image_id_list), строка 35*
- Записать изображение с помощью bk.addfile
- Исправить ссылку на файл, заменив внешнюю (из интернета), на локальную "../Images/%uniqueid%" банальным html.replace
- Сохранить наш html: bk.writefile(_id, html)
- Добавить обработку ошибок и вывод информации о том, что сейчас происходит.
- Запаковать в ZIP архив, при этом архив должен называться так же, как и папка (со скриптом и XML файлом), которая в него вложена.

Результат:
Исходный код: https://github.com/yastrov/imagedownloader-sigil-plugin
Релиз: https://github.com/yastrov/imagedownloader-sigil-plugin/releases

четверг, 5 мая 2016 г.

VirtualBox + Lubuntu 16.04

Проблема работы VirtualBox с Lubuntu 16.04, (когда при загрузке на экране некий графический шум), оказалось решается тривиально - переключением на текстовый терминал (Ctrl+Alt+F1), а потом возврат обратно (Ctrl+Alt+F7).

Solution to solve problem VirtualBox + Lubuntu 16.04 : go to text terminal (Ctrl+Alt+F1), and after return to GUI (Ctrl+Alt+F7).