Показаны сообщения с ярлыком Python. Показать все сообщения
Показаны сообщения с ярлыком Python. Показать все сообщения

суббота, 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

пятница, 22 марта 2013 г.

Matplotlib & кириллица

Сложно сказать, что было в древних версиях matplotlib, но последняя в репозитории Ubuntu 12.04 вполне замечательно дружит с кириллицей, если надписи передаются к строки юникод.(u"")