Прежде всего, я бы посоветовал против вендинга;несколько крупных пакетов раньше использовали вендоринг, но отошли, чтобы избежать необходимости справляться с вендорингом.Одним из таких примеров является 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)