![](/img/trans.png)
[英]Import a Third Party Python Project without Adding It to sys.path
[英]Import vendored dependencies in Python package without modifying sys.path or 3rd party packages
我正在为Anki 开发一系列附加组件,这是一个开源抽认卡程序。 Anki 附加组件作为 Python 包提供,基本文件夹结构如下所示:
anki_addons/
addon_name_1/
__init__.py
addon_name_2/
__init__.py
anki_addons
由基础应用程序附加到sys.path
,然后使用import <addon_name>
每个 add_on 。
我一直在尝试解决的问题是找到一种可靠的方法来使用我的附加组件传送包及其依赖项,同时不污染全局状态或退回到手动编辑供应商包。
具体来说,给定这样的附加结构......
addon_name_1/
__init__.py
_vendor/
__init__.py
library1
library2
dependency_of_library2
...
...我希望能够导入_vendor
目录中包含的任何任意包,例如:
from ._vendor import library1
相对于进口这样的主要困难是,他们没有(如,也要依赖于通过绝对引用进口其他的包工作,包import dependency_of_library2
中的源代码library2
)
到目前为止,我已经探索了以下选项:
import addon_name_1._vendor.dependency_of_library2
)。 但这是一项乏味的工作,不能扩展到更大的依赖树,也不能移植到其他包。sys.path.insert(1, <path_to_vendor_dir>)
在我的包初始化文件_vendor
添加到sys.path
。 这有效,但它引入了对模块查找路径的全局更改,这将影响其他附加组件甚至基础应用程序本身。 这似乎是一个可能导致潘多拉盒子问题的黑客行为(例如,同一软件包的不同版本之间的冲突等)。我已经被困在这个问题上好几个小时了,我开始认为我要么完全缺少一种简单的方法来做到这一点,要么我的整个方法存在根本性的错误。
有没有办法在我的代码中随附第三方包的依赖树,而不必求助于sys.path
hacks 或修改有问题的包?
编辑:
澄清一下:我无法控制如何从 anki_addons 文件夹导入附加组件。 anki_addons 只是基本应用程序提供的目录,所有附加组件都安装到其中。 它被添加到 sys 路径中,因此其中的附加包几乎就像位于 Python 模块查找路径中的任何其他 python 包一样。
首先,我建议不要使用vendoring; 一些主要的软件包之前确实使用了 vendoring,但是为了避免不得不处理 vendoring 的痛苦,它们已经转用了。 一个这样的例子是requests
库。 如果您依赖于使用pip install
来安装您的包的人,那么只需使用依赖项并告诉人们有关虚拟环境的信息。 不要假设您需要承担保持依赖关系解开的负担,或者需要阻止人们在全局 Python site-packages
位置安装依赖项。
同时,我很欣赏第三方工具的插件环境有所不同,如果向该工具使用的 Python 安装添加依赖项很麻烦或不可能,供应商化可能是一个可行的选择。 我看到 Anki 在没有 setuptools 支持的情况下将扩展名分发为.zip
文件,所以这肯定是这样的环境。
因此,如果您选择供应商依赖项,则使用脚本来管理您的依赖项并更新它们的导入。 这是您的选项 #1,但自动化。
这是pip
项目选择的路径,请参阅它们的自动化tasks
子目录,该子目录构建在invoke
库上。 请参阅 pip 项目vendoring README以了解他们的政策和理由(其中主要是pip
需要自行引导,例如,使其依赖项可用以能够安装任何东西)。
您不应使用任何其他选项; 您已经列举了#2 和#3 的问题。
使用自定义导入器的选项 #4 的问题是您仍然需要重写 import 。 换句话说, setuptools
使用的自定义导入器钩子根本没有解决供应商化的命名空间问题,相反,如果供应商化的包丢失,它可以动态导入顶级包( pip
通过手动解包过程解决的问题) )。 setuptools
实际上使用选项 #1,他们重写供应商包的源代码。 例如,请参阅setuptools
供应商子包中packaging
项目中的这些行; setuptools.extern
命名空间由自定义导入钩子处理,如果从供应商包导入失败,则重定向到setuptools._vendor
或顶级名称。
更新供应商软件包的pip
自动化采取以下步骤:
_vendor/
子目录中的文件,该除__init__.py
文件和要求的文本文件。pip
将所有 vendored 依赖项安装到该目录中,使用名为vendor.txt
的专用需求文件,避免编译.pyc
字节缓存文件并忽略瞬态依赖项(假设这些已在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
。requests
的pip
补丁是有趣的,因为它更新了requests
库已删除的供应商包的requests
库向后兼容层; 这个补丁很元! 所以本质上, pip
方法最重要的部分,vendored 包导入的重写非常简单; 解释为简化逻辑并删除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)
如何使您的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 *
我是新手,所以请耐心等待。
为了延续Martijn Pieters的精彩回复,自pip 20.0以来,pip 一直使用专用的 CLI 工具来供应依赖关系。 这个工具叫做vendoring ,似乎主要关注pip的需求,但我希望它可以成为任何有类似需求的项目的一个很好的框架。
在我写这篇评论时,他们还没有面向用户的文档: https : //github.com/pradyunsg/vendoring/issues/3
它可以通过pyproject.toml文件进行配置:
[tool.vendoring]
destination = "src/pip/_vendor/"
requirements = "src/pip/_vendor/vendor.txt"
namespace = "pip._vendor"
protected-files = ["__init__.py", "README.rst", "vendor.txt"]
patches-dir = "tools/vendoring/patches"
它可以安装在虚拟环境中,如下所示:
$ pip install vendoring
它似乎按如下方式工作:
$ vendoring sync /path/to/location # Install dependencies in destination folder
$ vendoring update /path/to/location # Update vendoring dependencies
捆绑依赖项的最佳方法是使用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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.