Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Создание zip-модулей в python

Anna | 15.06.2014 | нет комментариев

Предыстория

На определенном этапе становления спецтехнологий компании Acronis было принято решение исследовать вероятность распространения интерпретатора языка python3 собственной сборки как часть наших продуктов, расширив его при этом нашими собственными модулями, обеспечивающими доступ к инфраструктуре этих продуктов. Данный пост является одним из итогов изысканий в этом направлении.

В первую очередь, нам хотелось иметь ограниченный суперкомпактный комплект финальных распространяемых модулей. Впрочем публичная сборка питона, распространяемая через python.org к этому не располагает, одна только стандартная библиотека, являющаяся неотделимой частью самого языка, состоит из больше чем тысячи py-файлов. Именно следственно, мы сразу обратили внимание на такую любознательную специфика интерпретатора, как вероятность импорта модулей, находящихся в zip-архивах, когда все уйма исходников на питоне, относящихся к одному либо нескольким модулям, упаковано в zip-архив и распространяется одним zip-файлом.

Оглядываясь назад, дозволено с уверенностью сказать, что помощь работы с zip-модулями в питон — сильная и комфортная вещь. И она работает, причем работает отлично. Позже ряда экспериментов с zip-модулями, проникнувшись духом zip-пекеджинга, мы настоль вошли во вкус, что всю стандартную библиотеку языка питон (скриптовую ее часть) также упаковали в обособленный zip-файл.

Предисловие

Для начала сделаем тестовое окружение, максимально примитивное, но в то же время довольное для демонстрации всех обозначенных вероятностей обговариваемого функционала. Окружение будет виндовое, так уж мне оказалось в данный момент комфортнее. Для желающих испробовать приведенные тут примеры под линукс, легко подмечу, что принципиальных различий быть не должно, исключительное что требуется, это установленный python3, либо через пакетный администратор вашего линуксового дистрибутива, либо через ветхий добродушный configure/make/make install.

Примитивные демонстрационные модули, которые мы будем паковать в zip, у меня первоначально помещены в d:\habr\lib:

  • say_hello.py
    def say_hello():
        print("Hello python world.")
    
  • my_sysinfo/__init__.py
    from .sysinfo import print_sysinfo
    
  • my_sysinfo/sysinfo.py
    import sys
    import traceback
    
    def print_sysinfo():
        print(80 * '-')
        print(sys.version)
        print(80 * '-')
        traceback.print_stack()
        print(80 * '-')
    

От того что среди прочего хотелось продемонстрировать вероятность упаковки именно нескольких модулей в один zip-файл, тут я сотворил два разнотипных модуля, 1-й модуль say_hello состоит из одного файлаsay_hello.py c заиммплеменченной в нем функцией say_hello(), 2-й модуль my_sysinfo сделан чуть больше трудным – в виде директории с файлом __init__.py, содержащим в списке импорта функцию print_sysinfo. Забегая вперед, сразу скажу, что эта функция среди прочей сводной инфы типа sys.version также печатает стек собственного вызова именно для раскрытия особенностей zip-пекеджинга.

Проверяем, что все работает в неупакованном виде:

c:\Python33\python.exe
Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path.insert(0,'d:\\habr\\lib')
>>> import say_hello
>>> say_hello.say_hello()
Hello python world.
>>> import my_sysinfo
>>> my_sysinfo.print_sysinfo()
--------------------------------------------------------------------------------
3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)]
--------------------------------------------------------------------------------
  File "<stdin>", line 1, in <module>
  File "d:\habr\lib\my_sysinfo\sysinfo.py", line 9, in print_sysinfo
    traceback.print_stack()
--------------------------------------------------------------------------------

Упаковка в zip

В самойупаковке начальных py-файлов в zip никаких секретов нет. Для этого дозволено воспользоваться любым доступным под рукой zip-архиватором, либо упаковать прямо питон-скриптом, воспользовавшись для этого функционалом из стандартного модуля zipfile. Чуть позднее я приведу код простого скрипта упаковки, тот, что я назвал mkpyzip.py и положил в папку d:\habr\tools.

Упаковываем этим скриптом приведенные выше модули в zip-файл d:\habr\output\mybundle.zip:

С:\Python33\python.exe d:\habr\tools\mkpyzip.py --src d:\habr\lib\my_sysinfo d:\habr\lib\say_hello.py --out d:\habr\output\mybundle.zip
::: d:\habr\lib\my_sysinfo\__init__.py >>> mybundle.zip/my_sysinfo/__init__.py
::: d:\habr\lib\my_sysinfo\sysinfo.py >>> mybundle.zip/my_sysinfo/sysinfo.py
::: d:\habr\lib\say_hello.py >>> mybundle.zip/say_hello.py

В данный скрипт среди прочего добавлен подробнейший итог о том, какой файл и под каким именем упаковывается в zip-архив.

Проверяем, что все работает будучи упакованным в такой zip-архив:

c:\Python33\python.exe

Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path.insert(0, 'd:\\habr\\output\\mybundle.zip')
>>> import say_hello
>>> say_hello.say_hello()
Hello python world.
>>> import my_sysinfo
>>> my_sysinfo.print_sysinfo()
--------------------------------------------------------------------------------
3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)]
--------------------------------------------------------------------------------
  File "<stdin>", line 1, in <module>
  File "d:\habr\output\mybundle.zip\my_sysinfo\sysinfo.py", line 9, in print_sysinfo
    traceback.print_stack()
--------------------------------------------------------------------------------

Из оутпута видно, что все работает как положено, будучи упакованным в zip-архив, в частности распечатка стека из нашей функции my_sysinfo.print_sysinfo() показывает, что код вызываемой функции находится внутри нашего zip-файла — d:\habr\output\mybundle.zip\my_sysinfo\sysinfo.py

Генерация байт-кода при упаковке в zip


А теперь самое время припомнить о такой отлично знаменитой особенности интерпретатора, как генерация байт-кода при импорте модуля, либо загрузка и выполнение байт-кода сгенерированного ранее, если таковой является валидным в момент импорта. В случае с модулями упакованными в zip, дела обстоят несколько напротив. Для zip-модулей байт-код должен быть сгенерирован и упакован в zip-файл предварительно, напротив интерпретатор позже всякого перезапуска при импорте всякого модуля из zip-файла будет генерировать для него байт-код в памяти снова. Что ж, в нашем скрипте mkpyzip.py генерация байт-кода теснее предусмотрена, легко добавим опцию --mkpyc и перегенерим zip-файл:

c:\Python33\python.exe d:\habr\tools\mkpyzip.py --mkpyc --src d:\habr\lib\my_sysinfo d:\habr\lib\say_hello.py --out d:\habr\output\mybundle.zip
::: d:\habr\lib\my_sysinfo\__init__.py >>> mybundle.zip/my_sysinfo/__init__.py
::: mkpyc for: d:\habr\lib\my_sysinfo\__init__.py >>> mybundle.zip/my_sysinfo/__init__.pyc
::: d:\habr\lib\my_sysinfo\sysinfo.py >>> mybundle.zip/my_sysinfo/sysinfo.py
::: mkpyc for: d:\habr\lib\my_sysinfo\sysinfo.py >>> mybundle.zip/my_sysinfo/sysinfo.pyc
::: d:\habr\lib\say_hello.py >>> mybundle.zip/say_hello.py
::: mkpyc for: d:\habr\lib\say_hello.py >>> mybundle.zip/say_hello.pyc

Сейчас, когда раскрыты основные аспекты упаковки питон-модулей в zip-файл, самое время привести и код самой утилиты mkpyzip.py. Сразу подмечу, что ничего особенного в этом скрипте нет, а прототип для генерации байт-кода был позаимствован из стандартной библиотеки языка python (для поиска этого прототипа довольно сделать поиск по ключевому слову wr_long).

mkpyzip.py

import argparse
import imp
import io
import marshal
import os
import os.path
import zipfile

def compile_file(filename, codename, out):
    def wr_long(f, x):
        f.write(bytes([x & 0xff, (x >> 8)  & 0xff, (x >> 16) & 0xff, (x >> 24) & 0xff]))
    with io.open(filename, mode='rt', encoding='utf8') as f:
        source = f.read()
        ast = compile(source, codename, 'exec', optimize=1)
        st = os.fstat(f.fileno())
        timestamp = int(st.st_mtime)
        size = st.st_size & 0xFFFFFFFF
        out.write(b'')
        wr_long(out, timestamp)
        wr_long(out, size)
        marshal.dump(ast, out)
        out.flush()
        out.seek(0, 0)
        out.write(imp.get_magic())

def compile_in_memory(source, codename):
    with io.BytesIO() as fc:
        compile_file(source, codename, fc)
        return fc.getvalue()

def make_module_catalog(src):
    root_path = os.path.abspath(os.path.normpath(src))
    root_arcname = os.path.basename(root_path)
    if not os.path.isdir(root_path):
        return [(root_path, root_arcname)]
    catalog = []
    subdirs = [(root_path, root_arcname)]
    while subdirs:
        idx = len(subdirs) - 1
        subdir_path, subdir_archname = subdirs[idx]
        del subdirs[idx]
        for item in sorted(os.listdir(subdir_path)):
            if item == '__pycache__' or item.endswith('.pyc'):
                continue
            item_path = os.path.join(subdir_path, item)
            item_arcname = '/'.join([subdir_archname, item])
            if os.path.isdir(item_path):
                subdirs.append((item_path, item_arcname))
            else:
                catalog.append((item_path, item_arcname))
    return catalog

def mk_pyzip(sources, outzip, mkpyc=False):
    zipfilename = os.path.abspath(os.path.normpath(outzip))
    display_zipname = os.path.basename(zipfilename)
    with zipfile.ZipFile(zipfilename, "w", zipfile.ZIP_DEFLATED) as fzip:
        for src in sources:
            catalog = make_module_catalog(src)
            for entry in catalog:
                fname, arcname = entry[0], entry[1]
                fzip.write(fname, arcname)
                print("::: {} >>> {}/{}".format(fname, display_zipname, arcname))
                if mkpyc and arcname.endswith('.py'):
                    bytes = compile_in_memory(fname, arcname)
                    pyc_name = ''.join([os.path.splitext(arcname)[0], '.pyc'])
                    fzip.writestr(pyc_name, bytes)
                    print("::: mkpyc for: {} >>> {}/{}".format(fname, display_zipname, pyc_name))

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--src', nargs=' ', required=True)
    parser.add_argument('--out', required=True)
    parser.add_argument('--mkpyc', action='store_true')
    args = parser.parse_args()
    mk_pyzip(args.src, args.out, args.mkpyc)

if __name__ == '__main__':
    main()

Валидность байт-кода


Добавлю также пару слов о том, как убедится в том, что сгенерированный нами байт-код валиден и интерпретатор типично его подхватывает при импорте модуля без попыток перегенерации нового байт-кода в памяти.
Для этого легко распечатаем признак __file__, у заимпорченого модуля say_hello.

c:\Python33\python.exe

Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path.insert(0,'d:\\habr\\output\\mybundle.zip')
>>> import say_hello
>>> say_hello.__file__
'd:\\habr\\output\\mybundle.zip\\say_hello.pyc'

То, что признак __file__ у загруженного модуля указывает на сгенерированный нами pyc-файл, является довольным доказательством валидности нашего байт-кода.

На этом я бы вероятно мог с чистой совестью завершить свой вводный обзор о zip-пекеджинге в языке питон, если бы не одно «но»…

Сюрпризы


Один из моих коллег как-то раз взял в руки Eclipse и с поддержкой отлично знаменитого к нему дополненияPyDev попытался заняться отладкой написанного им питон-скрипта, использовавшего среди прочего также и функционал и из питон-модулей зазипованых по только что описанной спецтехнологии.

Стержневой малоприятный сюрприз заключался в том, что PyDev подчистую отказывался сходственные модули дебажить. Крепко заинтересовавшись этой неприятностью мы начали искать первоисточник задачи. Теперь, оглядываясь теснее назад, мы можем сказать, что по нашему личному убеждению в PyDev легко неудовлетворительно добротная помощь для отладки zip-модулей.

Тем не менее в момент изыскания нюансы отладки под PyDev были сразу же исключены из рассмотрения, т.к. встроенный в питон отладчик pdb также выдавал информацию о стеке вызовов крайне подозрительного вида. Причем информация была подозрительной только в случае, когда в zip-архиве наравне с начальными py-файлами также находились и pyc-файлы с байт-кодом. В случае же zip-архива с одними только py-файлами механически генерируемый байт-код очевидно чем-то отличался, и отладка в pdb давала положительную информацию, не вызывавшую нареканий. За исключением отладки все работало как положено. И тем не менее с нашим байт-кодом было что-то определенно не то. И об этом нам очевидно сигнализировал pdb.

Сейчас, когда первоисточник задачи нами обнаружен, вдаваться в детали отладки питон-кода под pdb теснее не хочется. Дабы пояснить причину задачи, давайте легко снова распечатаем стек вызовов из зазипованого байт-кода, воспользовавшись написанной ранее функцией print_sysinfo() из модуля my_sysinfo.

c:\Python33\python.exe
Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path.insert(0,'d:\\habr\\output\\mybundle.zip')
>>> import my_sysinfo
>>> my_sysinfo.print_sysinfo()
--------------------------------------------------------------------------------
3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)]
--------------------------------------------------------------------------------
  File "<stdin>", line 1, in <module>
  File "my_sysinfo/sysinfo.py", line 9, in print_sysinfo
    traceback.print_stack()
--------------------------------------------------------------------------------

Сейчас сравним данный оутпут с тем, тот, что был получен нами ранее, еще до того, как мы начали зиповать наш личный байт-код. Ключевое различие тут в путях к файлу в стековом фрейме.

Без байт-кода в zip-файле у нас был оутпут вида:
File «stdin», line 1, in «module»
File “d:\habr\output\mybundle.zip\my_sysinfo\sysinfo.py“, line 9, in print_sysinfo
traceback.print_stack()

А позже добавления байт-кода он принял вид:
File «stdin», line 1, in «module»
File “my_sysinfo/sysinfo.py“, line 9, in print_sysinfo
traceback.print_stack()

Из оутпута становится Отчетливо видно, что при добавлении байт-кода в zip-архив в стеке вызовов путь к файлу из безусловного пути превращается в относительный, причем относительный по отношению к корню zip-архива. Здесь внимательный читатель может сразу возразить, что мы же сами сгенерировали такой байт-код, подав данный относительный путь в builtin-функцию compile в утилите mkpyzip.py. Но если об этом поразмыслить чуть глубже, то становится ясно, что полный то путь в данном случае никак не уместен, потому как финальная наша цель — это, собрав zip-архив на одной машине, иметь вероятность применять его на иной, допустимо даже на машине с иной операционной системой.

Никто из нас на тот момент не был близко знаком с имлементацией загрузки zip-модулей в интерпретатор, следственно немыслимо было дать однозначного результата на вопрос, в чем же корень задачи: то ли мы по незнанию что-то упускаем при генерации байт-кода, то ли сам загрузчик zip-модулей в питон ведет себя некорректно при его загрузке.

В результате было решено обратиться за советом к самим разработчикам языка питон через python-dev@python.org. Исключительное, что они нам порекомендовали на тот момент, это завести на эту тему баг, что бы не потерялся контекст описанной задачи. Баг мы завели bugs.python.org/issue18307 и стали ожидать. Приблизительно позже месяца ожидания и занятий другими не менее насущными задачами наше терпение тихо кончилось, и python33.dll попал в отладчик.

В результате мы удостоверили наши сомнения и с определенностью можем утверждать, что именно Си-шная имплементация загрузчика zip-модулей в питон ведет себя некорректно при загрузке байт-кода. Вернее то, что описанный тут случай, требующий механической нормализации путей в байт-коде при его загрузке из zip-файлов, был легко не реализован. В результате в рамках этого же бага мы предложили патч, исправляющий данную задачу, и приводящий пути к файлам в стеках вызовов к безусловному виду.

Теперь, приблизительно пол-года через, данный баг на bugs.python.org остается открытым. Видимо потому, что zip-модули в питон — фича, правда и сильная, но довольно редко применяемая, исключительно случай с байт-кодом внутри zip-архива. Тем не менее, имея личный репозитарий с исходниками питона (тот, что мы по вероятности усердствуем удерживать максимально близким к публичному оригиналу), мы легко закоммитили к себе данный патч.

Завершение

Модули в питон, будучи запакованными в zip-архив, работают также отлично, как и в неупакованном виде. Исключительное к чему нужно быть готовым, что позже упаковки могут появиться определенные трудности с их отладкой как через Eclipse PyDev, так и через другие IDE, отладка в которых также основана на PyDev. Тем не менее в определенных обстановках вероятность иметь суперкомпактное уйма бинарных продакшн-модулей может оказаться гораздо главней легкой отладки питон-кода в IDE.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB