Ответ 1
Прежде всего, я бы советовал против продажи; в нескольких крупных пакетах раньше использовалось предложение, но они отказались, чтобы избежать боли при работе с поставщиками. Одним из таких примеров является библиотека requests
. Если вы полагаетесь на людей, использующих pip install
для установки вашего пакета, просто используйте зависимости и расскажите людям о виртуальных средах. Не предполагайте, что вам нужно взять на себя бремя удержания зависимостей незанятыми или заставить людей не устанавливать зависимости в глобальном местоположении site-packages
Python.
В то же время я ценю, что подключаемая среда стороннего инструмента - это нечто иное, и если добавление зависимостей к установке 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
. - Примените несколько патчей, чтобы вытереть оставшиеся изменения; с точки зрения vendoring только
pip
патч дляrequests
интересно здесь в том, что он обновляетrequests
библиотеки обратной слой совместимости для vendored пакетов, что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 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)