jjzjj

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

coder 2023-08-13 原文

概要

我正在为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)

解决方案尝试

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


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

    我是否可以在不借助sys.path hack或修改有问题的软件包的情况下随代码附带第三方软件包的依赖树?

    编辑:

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

    最佳答案

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

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

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

    这是pip项目选择的路径,有关其自动化的信息,请参见 tasks subdirectory,该路径建立在 invoke library之上。请参阅pip项目vendoring README了解其政策和基本原理(其中的主要之处是pip需要自举,例如,可以使用其依赖项来安装任何东西)。

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

    使用自定义导入程序的选项#4的问题在于,您仍然需要重写导入。换句话说,setuptools使用的自定义导入程序钩子(Hook)根本无法解决供应商 namespace 的问题,相反,如果缺少供应商化的软件包( pip solves with a manual debundling process这个问题),则可以动态导入顶级软件包。 setuptools实际上使用选项#1,在那里他们重写供应商软件包的源代码。参见packaging供应子包中的these lines in the setuptools projectsetuptools.extern命名空间由自定义导入挂钩处理,如果从供应商化程序包的导入失败,则它将重定向到setuptools._vendor或顶级名称。

    用来更新供应商软件包的pip自动化执行以下步骤:

  • 删除_vendor/子目录中的所有内容,文档,__init__.py文件和需求文本文件除外。
  • 使用专用的需求文件pip,使用vendor.txt将所有供应商的依赖项安装到该目录中,避免编译.pyc字节缓存文件并忽略瞬时依赖项(假定这些已经在vendor.txt中列出了);使用的命令是pip install -t pip/_vendor -r pip/_vendor/vendor.txt --no-compile --no-deps
  • 删除pip已安装但在供应商环境中不需要的所有内容,即*.dist-info*.egg-infobin目录以及已安装的依赖项中的一些内容,这些内容是pip永远不会使用的。
  • 收集所有已安装目录并添加文件,但不带.py扩展名(因此白名单中没有任何内容);这是vendored_libs列表。
  • 重写导入;这只是一系列正则表达式,其中vendored_lists中的每个名称都用import <name>替换import pip._vendor.<name>出现,而每个from <name>(.*) import替换每个from pip._vendor.<name>(.*) import出现。
  • 应用一些补丁来清除剩余的所需更改;从供应商的 Angular 来看,这里只有pip patch for 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)
    

    关于python - 在不修改sys.path或第三方软件包的情况下,在Python软件包中导入供应商依赖性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52538252/

    有关python - 在不修改sys.path或第三方软件包的情况下,在Python软件包中导入供应商依赖性的更多相关文章

    1. ruby - 默认情况下使选项为 false - 2

      这是在Ruby中设置默认值的常用方法:classQuietByDefaultdefinitialize(opts={})@verbose=opts[:verbose]endend这是一个容易落入的陷阱:classVerboseNoMatterWhatdefinitialize(opts={})@verbose=opts[:verbose]||trueendend正确的做法是:classVerboseByDefaultdefinitialize(opts={})@verbose=opts.include?(:verbose)?opts[:verbose]:trueendend编写Verb

    2. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

      我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

    3. ruby - 在不使用 RVM 的情况下在 Mac 上卸载和升级 Ruby - 2

      我最近决定从我的系统中卸载RVM。在thispage提出的一些论点说服我:实际上,我的决定是,我根本不想担心Ruby的多个版本。我只想使用1.9.2-p290版本而不用担心其他任何事情。但是,当我在我的Mac上运行ruby--version时,它告诉我我的版本是1.8.7。我四处寻找如何简单地从我的Mac上卸载这个Ruby,但奇怪的是我没有找到任何东西。似乎唯一想卸载Ruby的人运行linux,而使用Mac的每个人都推荐RVM。如何从我的Mac上卸载Ruby1.8.7?我想升级到1.9.2-p290版本,并且我希望我的系统上只有一个版本。 最佳答案

    4. Tomcat AJP 文件包含漏洞(CVE-2020-1938) - 2

      目录1.漏洞简介2、AJP13协议介绍Tomcat主要有两大功能:3.Tomcat远程文件包含漏洞分析4.漏洞复现 5、漏洞分析6.RCE实现的原理1.漏洞简介2020年2月20日,公开CNVD的漏洞公告中发现ApacheTomcat文件包含漏洞(CVE-2020-1938)。ApacheTomcat是Apache开源组织开发的用于处理HTTP服务的项目。ApacheTomcat服务器中被发现存在文件包含漏洞,攻击者可利用该漏洞读取或包含Tomcat上所有webapp目录下的任意文件。该漏洞是一个单独的文件包含漏洞,依赖于Tomcat的AJP(定向包协议)。AJP自身存在一定缺陷,导致存在可控

    5. 软件测试基础 - 2

      Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功

    6. ruby - 将一个超薄文件包含在另一个超薄文件中 - 2

      我在一个静态网站上工作(因此没有真正的服务器支持),我想在另一个网站中包含一个小的细长片段,可能会向它传递一个变量。这可能吗?在rails中很容易,虽然是render方法,但我不知道如何在slim上做(显然load方法不适用于slim)。 最佳答案 Slim包含Include插件,允许在编译时直接在模板文件中包含其他文件:require'slim/include'includepartial_name文档可在此处获得:https://github.com/slim-template/slim/blob/master/doc/incl

    7. ruby - 在什么情况下会使用 Sinatra 或 Merb? - 2

      我正在学习Rails,对Sinatra和Merb知之甚少。我想知道您会在哪些情况下使用Merb/Sinatra。感谢您的反馈! 最佳答案 Sinatra是一个比Rails更小、更轻的框架。如果你想让一些东西快速运行,只需发送几个URL并返回一些简单的内容,就可以使用它。看看Sinatrahomepage;这就是启动和运行“Hello,World”所需的全部内容,而在Rails中,您需要生成整个项目结构、设置Controller和View、设置路由等等(我还没有有一段时间写了一个Rails应用程序,所以我不知道“Hello,World

    8. ruby - 是否可以在不实际发送或读取数据的情况下查明 ruby​​ 套接字是否处于 ESTABLISHED 或 CLOSE_WAIT 状态? - 2

      s=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)s.connect(Socket.pack_sockaddr_in('port','hostname'))ssl=OpenSSL::SSL::SSLSocket.new(s,sslcert)ssl.connect从这里开始,如果ssl连接和底层套接字仍然是ESTABLISHED,或者它是否在默认值7200之后进入CLOSE_WAIT,我想检查一个线程几秒钟甚至更糟的是在实际上不需要.write()或.read()的情况下关闭。是用select()、IO.select()还是其他方法完成

    9. ruby-on-rails - 在这种情况下我如何模拟一个对象?没有明显的方法可以用模拟替换对象 - 2

      假设我在Store的模型中有这个非常简单的方法:defgeocode_addressloc=Store.geocode(address)self.lat=loc.latself.lng=loc.lngend如果我想编写一些不受地理编码服务影响的测试脚本,这些脚本可能已关闭、有限制或取决于我的互联网连接,我该如何模拟地理编码服务?如果我可以将地理编码对象传递到该方法中,那将很容易,但我不知道在这种情况下该怎么做。谢谢!特里斯坦 最佳答案 使用内置模拟和stub的rspecs,你可以做这样的事情:setupdo@subject=MyCl

    10. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

      因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

    随机推荐