在不修改sys.path或第三方软件包的情况下,在Python软件包中导入供应商依赖性

谷氨酸盐:

摘要

我正在为Anki(开源抽认卡程序)开发一系列附加组件Anki附件以Python软件包的形式提供,其基本文件夹结构如下所示:

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

anki_addonssys.path由基本应用程序附加到,然后由导入每个add_on import <addon_name>

我一直试图解决的问题是找到一种可靠的方式来随我的附件一起运送软件包及其依赖项,同时又不污染全局状态或不依赖于对供应商软件包的手动编辑

细节

具体来说,给定这样的附加结构...

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

...我希望能够导入_vendor目录中包含的任何任意软件包,例如:

from ._vendor import library1

这样的相对导入的主要困难在于,它们不适用于还依赖于通过绝对引用导入的其他软件包的软件包(例如import dependency_of_library2在的源代码中library2

解决方案尝试

到目前为止,我已经探索了以下选项:

  1. 手动更新第三方软件包,以便它们的import语句指向我的python软件包(例如import addon_name_1._vendor.dependency_of_library2)中的标准模块路径但这是一件繁琐的工作,无法扩展到较大的依赖树,也无法移植到其他软件包。
  2. 添加_vendorsys.path通过sys.path.insert(1, <path_to_vendor_dir>)我的包中初始化文件。这可行,但是它对模块查找路径进行了全局更改,这将影响其他加载项,甚至影响基本应用程序本身。似乎这是一种黑客行为,可能会在以后导致pandora出现一系列问题(例如,同一软件包的不同版本之间发生冲突等)。
  3. 为我的进口临时修改sys.path ; 但这不适用于使用方法级导入的第三方模块。
  4. 根据我在setuptools中找到的示例编写PEP302样式的自定义导入程序,但是我无法做到这一点。

我已经在这个问题上停留了好几个小时,而且我开始认为我要么完全错过了执行此操作的简单方法,要么我的整个方法都存在根本性的错误。

有没有办法在我的代码中附带第三方软件包的依赖树,而不必求助于sys.path黑客或修改相关软件包?


编辑:

只是要澄清一下:我无法控制如何从anki_addons文件夹中导入加载项。anki_addons只是基本应用程序提供的目录,所有附加组件均安装在该目录中。它被添加到sys路径中,因此其中的附加程序包的行为几乎与位于Python模块查找路径中的任何其他python程序包一样。

马丁·彼得斯(Martijn Pieters):

首先,我建议不要出售。一些主要软件包以前曾使用过供应商,但是为了避免不得不处理供应商的痛苦,已经放弃了。requests图书馆就是一个这样的例子如果您依靠pip install用来安装软件包的人员,则只需使用依赖项并向人们介绍虚拟环境。不要假设您需要承担使依赖关系混乱的负担,也不必阻止人们在全局Python site-packages位置中安装依赖项

同时,我很欣赏第三方工具的插件环境有所不同,并且如果对该工具使用的Python安装添加依赖项很麻烦或无法进行商业销售可能是一个可行的选择。我看到Anki在.zip不支持setuptools的情况下将扩展名分发为文件,因此肯定是这种环境。

因此,如果您选择供应商依赖性,请使用脚本来管理依赖性并更新其导入。这是您的选择#1,但自动化

这是pip项目选择的路径,有关其自动化的信息,请参见其tasks子目录,该子目录建立在invoke库上有关策略和原理,请参阅pip项目供应商README(其中的一个主要内容是pip需要自举,例如,使其依赖项可用于安装任何东西)。

您不应使用任何其他选项;您已经列举了#2和#3的问题。

使用自定义导入程序的选项#4的问题在于,您仍然需要重写import换句话说,所使用的自定义导入器钩子setuptools根本无法解决供应商名称空间的问题,而是可以在缺少供应商软件包的情况下动态导入顶级软件包(pip通过手动分包过程解决的问题)。setuptools实际上使用选项#1,在那里他们重写供应商软件包的源代码。例如,在packaging项目setuptools供应商子包中查看这些行setuptools.extern命名空间是由自定义导入钩,然后重定向要么处理setuptools._vendor 或顶级名称(如果从供应商化的软件包导入失败)。

pip自动化更新vendored包采取以下步骤:

  • 删除所有_vendor/子目录,除了文档的__init__.py文件和要求的文本文件。
  • 用于使用pip名为的专用需求文件将所有供应商的依赖项安装到该目录中,vendor.txt避免编译.pyc字节缓存文件并忽略瞬时依赖项(假定vendor.txt在其中列出);使用的命令是pip install -t pip/_vendor -r pip/_vendor/vendor.txt --no-compile --no-deps
  • 这是由安装删除一切pip,但在vendored环境中不需要的,即*.dist-info*.egg-infobin目录,并从已安装的依赖关系的几件事情pip永远不会使用。
  • 收集所有已安装的目录,并添加没有.py扩展名的文件(因此白名单中没有任何内容);这是vendored_libs清单。
  • 重写进口;这仅仅是一系列的正则表达式的,其中每一个名字vendored_lists被用来替换import <name>发生与import pip._vendor.<name>一位from <name>(.*) import有发生from pip._vendor.<name>(.*) import
  • 应用一些补丁以清除所需的其余更改;从供应商的角度来看,这里只有pip 补丁requests才是有趣的,因为它requestsrequests库已删除的供应商软件包更新了库的向后兼容性层这个补丁是相当元的!

因此,从本质上讲,该方法中最重要的部分是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)

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

在不修改第三方代码的情况下取消警告

如何在不修改第三方元素的情况下替换:: shadow

带有供应商/第三方库的extjs软件包构建

在不修改 sys.path 的情况下在外部目录中查找类

如何在没有d.ts文件的情况下导入第三方软件包?

在Laravel中安全地编辑第三方作曲家(供应商)软件包,并防止在发行新版本的软件包时丢失自定义更改

如何使样式不修改外部包样式?

Selenium-在不修改PATH的情况下运行ChromeDriver

反应原生。第三方包中的修改

Python dict删除内联而不修改

Python不修改全局变量

去安装第三方软件包,无法识别的导入路径

如何构建和分发依赖第三方libFoo.so的Python / Cython软件包

Python覆盖第三方软件包单个文件

不使用第三方软件包的嵌套for循环的python打印进度

如何在不修改限制的情况下进行分页?

在不修改类的情况下忽略动态属性的类型?

在不修改系统的情况下测试应用程序

Python中软件包的供应商名称空间

修改RPM软件包以删除依赖项

安装并导入修改后的软件包

如何使用RPM规范文件概括第三方依赖软件包的版本?

无业的供应者为什么不修改〜/ .bashrc?

从第三方删除安装的Deb软件包

无法从Django中的第三方软件包获取反向URL

Golang中的版本控制第三方软件包

使用带有第三方软件包的dotnet构建.net标准库

从第三方存储库apt-get install软件包

如何在Java中使用第三方软件包