Импортируйте зависимости от поставщиков в пакет Python без изменения пакетов sys.path или сторонних производителей. - PullRequest
0 голосов
/ 27 сентября 2018

Резюме

Я работаю над серией дополнений для Anki , программы с открытым исходным кодом.Дополнения Anki поставляются в виде пакетов Python, базовая структура папок выглядит следующим образом:

anki_addons/
    addon_name_1/
        __init__.py
    addon_name_2/
        __init__.py

anki_addons добавляется в sys.path базовым приложением, которое затем импортирует каждое дополнение с * 1010.*.

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

Особенности

В частности, с учетом такой структуры дополнения ...

addon_name_1/
    __init__.py
    _vendor/
        __init__.py
        library1
        library2
        dependency_of_library2
        ...

... Я хотел бы иметь возможностьимпортировать любой произвольный пакет, который включен в каталог _vendor, например:

from ._vendor import library1

Основная трудность при относительном импорте, подобном этому, заключается в том, что он не работает для пакетов, которые также зависят от других пакетов, импортированных через абсолютныйссылки (например, import dependency_of_library2 в исходном коде library2)

попытки решения

До сих пор я исследовал следующие варианты:

  1. Обновление вручнуюсторонние пакеты, так что их операторы импорта указывают на полный путь к модулю в моем пакете python (например, import addon_name_1._vendor.dependency_of_library2).Но это утомительная работа, которая не масштабируется для больших деревьев зависимостей и не переносится на другие пакеты.
  2. Добавление _vendor в sys.path через sys.path.insert(1, <path_to_vendor_dir>) в файле инициализации моего пакета.Это работает, но оно вносит глобальное изменение в путь поиска модуля, которое повлияет на другие надстройки и даже само базовое приложение.Это просто похоже на взлом, который может привести к тому, что в дальнейшем у пандоры возникнут проблемы (например, конфликты между различными версиями одного и того же пакета и т. Д.).
  3. Временное изменение sys.path для моегоимпорт ;но это не работает для сторонних модулей с импортом на уровне метода.
  4. Запись пользовательского импортера в стиле PEP302 на основе примера, который я нашел в setuptools ,но я просто не мог осознать это.

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

Нет ли способа отправить дерево зависимостей сторонних пакетов с моим кодом безВам приходится прибегать к sys.path взломам или изменению пакетов?


Редактировать:

Просто чтобы уточнить: я не имею никакого контроля над тем, как добавитьОни импортируются из папки anki_addons.anki_addons - это просто каталог, предоставляемый базовым приложением, в который установлены все надстройки.Он добавляется в путь sys, поэтому пакеты в нем в основном ведут себя как любой другой пакет python, расположенный в путях поиска модулей Python.

Ответы [ 3 ]

0 голосов
/ 06 октября 2018

Лучший способ связать зависимости - это использовать virtualenv.Проект Anki должен, по крайней мере, быть в состоянии установить внутри него.

Я думаю, что вы ищете, это namespace packages.

https://packaging.python.org/guides/packaging-namespace-packages/

Я мог бы представитьчто основной проект Anki имеет setup.py, а каждая надстройка имеет свой setup.py и может быть установлена ​​из своего исходного дистрибутива.Затем надстройки могут перечислять свои зависимости в своих собственных setup.py, и pip установит их в site-packages.

Пакеты пространств имен только решают часть проблемы, и, как вы сказали, у вас нет никакого контролянад тем, как надстройки импортируются из папки anki_addons.Я думаю, что проектирование того, как надстройки импортируются и упаковываются, идет рука об руку.

Модуль pkgutil предоставляет основному проекту способ обнаружить установленные надстройки.https://packaging.python.org/guides/creating-and-discovering-plugins/

Проект, который широко использует это Zope.http://www.zope.org

Посмотрите здесь: https://github.com/zopefoundation/zope.interface/blob/master/setup.py

0 голосов
/ 06 октября 2018

Прежде всего, я бы посоветовал против вендинга;несколько крупных пакетов раньше использовали вендоринг, но отошли, чтобы избежать необходимости справляться с вендорингом.Одним из таких примеров является requests библиотека .Если вы полагаетесь на людей, использующих pip install для установки вашего пакета, то просто используйте зависимости и расскажите людям о виртуальных средах.Не думайте, что вам нужно взять на себя бремя сохранения взаимосвязей или мешать людям устанавливать зависимости в глобальном местоположении Python site-packages.

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

Так что, если вы решите использовать зависимости поставщиков, используйте сценарий для управления вашими зависимостями и обновления их импорта.Это ваш вариант № 1, но автоматизирован .

Это путь, выбранный проектом pip, см. Их подкаталог tasks для их автоматизации, основанный на библиотеке invoke .См. Pip-проект , предлагающий README , для ознакомления с их политикой и обоснованием (главное из них заключается в том, что pip необходимо самому загрузить , например, иметь свои зависимости для установки чего-либо).

Вы не должны использовать другие опции;Вы уже перечислили проблемы с № 2 и № 3.

Проблема с вариантом № 4, использующим пользовательский импортер, заключается в том, что вам все еще нужно переписать импорт .Иными словами, пользовательский обработчик импорта, используемый setuptools, вообще не решает проблему пространства имен вендора, вместо этого он позволяет динамически импортировать пакеты верхнего уровня, если вендорные пакеты отсутствуют (проблема, которая pip решает с ручным процессом устранения неполадок ).setuptools фактически использует параметр # 1, где они переписывают исходный код для вендорных пакетов.См., Например, эти строки в packaging проекте в фирменном подпакете setuptools;пространство имен setuptools.extern обрабатывается настраиваемым хуком импорта, который затем перенаправляется либо на setuptools._vendor, либо на имя верхнего уровня в случае сбоя импорта из вендоренного пакета.

Автоматизация pip для обновления вендорных пакетоввыполняет следующие шаги:

  • Удаляет все в подкаталоге _vendor/, за исключением документации, файла __init__.py и текстового файла требований.
  • Использованиеpip для установки всех поставщиков зависимостей в этот каталог, используя специальный файл требований с именем vendor.txt, избегая компиляции файлов .pyc bytecache и игнорируя временные зависимости (предполагается, что они уже перечислены в vendor.txt);Используемая команда: pip install -t pip/_vendor -r pip/_vendor/vendor.txt --no-compile --no-deps.
  • Удалите все, что было установлено с помощью pip, но не требуется в среде поставщика, то есть *.dist-info, *.egg-info, каталог bin и некоторые вещи.из установленных зависимостей, которые pip никогда бы не использовал.
  • Соберите все установленные каталоги и добавленные файлы без расширения .py (так, чтобы ничего не попало в белый список);это список vendored_libs.
  • Переписать импорт;это просто серия регулярных выражений, где каждое имя в vendored_lists используется для замены import <name> вхождений import pip._vendor.<name>, а каждое from <name>(.*) import вхождений from pip._vendor.<name>(.*) import.
  • . Применение нескольких патчей для швабры.до оставшихся необходимых изменений;с точки зрения вендора, здесь интересен только патч pip для requests, поскольку он обновляет уровень обратной совместимости библиотеки requests для вендорных пакетов, которые была удалена библиотекой requests;этот патч довольно мета!

Так что, по сути, самая важная часть подхода pip, переписать импорт пакетов, предлагаемых фирмой, довольно просто;Перефразируя для упрощения логики и удаления pip определенных частей, это просто следующий процесс:

import shutil
import subprocess
import re

from functools import partial
from itertools import chain
from pathlib import Path

WHITELIST = {'README.txt', '__init__.py', 'vendor.txt'}

def delete_all(*paths, whitelist=frozenset()):
    for item in paths:
        if item.is_dir():
            shutil.rmtree(item, ignore_errors=True)
        elif item.is_file() and item.name not in whitelist:
            item.unlink()

def iter_subtree(path):
    """Recursively yield all files in a subtree, depth-first"""
    if not path.is_dir():
        if path.is_file():
            yield path
        return
    for item in path.iterdir():
        if item.is_dir():
            yield from iter_subtree(item)
        elif item.is_file():
            yield item

def patch_vendor_imports(file, replacements):
    text = file.read_text('utf8')
    for replacement in replacements:
        text = replacement(text)
    file.write_text(text, 'utf8')

def find_vendored_libs(vendor_dir, whitelist):
    vendored_libs = []
    paths = []
    for item in vendor_dir.iterdir():
        if item.is_dir():
            vendored_libs.append(item.name)
        elif item.is_file() and item.name not in whitelist:
            vendored_libs.append(item.stem)  # without extension
        else:  # not a dir or a file not in the whilelist
            continue
        paths.append(item)
    return vendored_libs, paths

def vendor(vendor_dir):
    # target package is <parent>.<vendor_dir>; foo/_vendor -> foo._vendor
    pkgname = f'{vendor_dir.parent.name}.{vendor_dir.name}'

    # remove everything
    delete_all(*vendor_dir.iterdir(), whitelist=WHITELIST)

    # install with pip
    subprocess.run([
        'pip', 'install', '-t', str(vendor_dir),
        '-r', str(vendor_dir / 'vendor.txt'),
        '--no-compile', '--no-deps'
    ])

    # delete stuff that's not needed
    delete_all(
        *vendor_dir.glob('*.dist-info'),
        *vendor_dir.glob('*.egg-info'),
        vendor_dir / 'bin')

    vendored_libs, paths = find_vendored_libs(vendor_dir, WHITELIST)

    replacements = []
    for lib in vendored_libs:
        replacements += (
            partial(  # import bar -> import foo._vendor.bar
                re.compile(r'(^\s*)import {}\n'.format(lib), flags=re.M).sub,
                r'\1from {} import {}\n'.format(pkgname, lib)
            ),
            partial(  # from bar -> from foo._vendor.bar
                re.compile(r'(^\s*)from {}(\.|\s+)'.format(lib), flags=re.M).sub,
                r'\1from {}.{}\2'.format(pkgname, lib)
            ),
        )

    for file in chain.from_iterable(map(iter_subtree, paths)):
        patch_vendor_imports(file, replacements)

if __name__ == '__main__':
    # this assumes this is a script in foo next to foo/_vendor
    here = Path('__file__').resolve().parent
    vendor_dir = here / 'foo' / '_vendor'
    assert (vendor_dir / 'vendor.txt').exists(), '_vendor/vendor.txt file not found'
    assert (vendor_dir / '__init__.py').exists(), '_vendor/__init__.py file not found'
    vendor(vendor_dir)
0 голосов
/ 03 октября 2018

Как насчет сделать вашу папку anki_addons пакетом и импортировать необходимые библиотеки в __init__.py в папке основного пакета.

Так что это будет что-то вроде

anki/
__init__.py

В anki.__init__.py:

from anki_addons import library1

В anki.anki_addons.__init__.py:

from addon_name_1 import *

Я новичок в этом, поэтому, пожалуйста, потерпите меня здесь.

...