← 返回文章流

在 Python 中使用 vendor 的方法

本文介绍了在 Python 库中 vendor 第三方库的正确方法。我知道这篇文章的受众非常狭窄,大部分 Python 开发者都不会也不需要用到这个技术, 但是本着分享的精神还是把它总结一二,作为软件的作者更是应该尊重所有其他库的作者的劳动。 <!-- more --> WHAT - vendor 是什么? Vendor,直译供应商,在软件中(比如 C, Go 等语言中),是一种把第三方库的代码直接内嵌到软件中的方式。 它不同于通过依赖文件指定的方式,第三方库的代码是直接包含在软件中的,有可能原样保留也有可能经过修改,所以需要注意各种 License 的限制, 特别是如果上游库采用了 GPL 系列的协议,使用 vendor 的软件也是要受到传染的。 WHY - Python 中什么时候要用到 vendor? 正如我开头说的,适用范围非常狭窄,有三种场景: 软件特性限制其必须是自包含,零依赖的。 在 Python 的世界中,最重度使用 vendor 的库就是我们天天都要用的 pip。pip._vendor 中包含了 25 个依赖。pip 是现行标准的 Python 安装器,所以它不能 有任何依赖,否则为了装 pip,要先装这些依赖,而这些依赖又只能通过 pip 安装,这就递归了。除此之外,还包括像 setuptools 这样的基础构建工具。 软件依赖某上游库的特定版本。这还包含上游库频繁 breaking change,导致 API 不稳定的情况。如果简单地在依赖中指定 third-party-lib==1.0.0, 会导致与之共存的同样依赖此库的软件无法解析版本,造成依赖冲突。而如果转成 vendor,就相当于把这个非常严格的依赖限制去掉了。 软件需要对上游库作一些变更,而由于上游库的维护问题,这些变更无法通过 PR 等方式合入上游并发布。在符合开源协议约束的情况下,可以通过 vendor 把源代码嵌入到软件中 并自行修改。 其实,针对上述的第 2、3 种场景,也不是非 vendor 不可。除了 vendor,还可以 fork 到自己的 git 仓库,再使用 git 依赖 引入,或者发布为一个新的 PyPI 包。只是 vendor 是一个最轻松的方式。 还有一个限制条件:对 Python 来说,只有纯 Python 的库才能 vendor。 HOW - 应该如何 vendor? vendor 并不是简单地复制粘贴这种传统艺能就解决了的,在我看来,它还要注意以下两点: vendor 必须要遵守开源协议,并把协议文件也放到 vendor 目录中。 对源代码有修改时,需要记录 patch 文件,以便时机成熟时,反馈回上游。 所以,vendor 并不是复制粘贴,只是在开源框架下对现状的一种妥协,我们最终的目标,是消灭 vendor。 在 Python 中,除了把 vendor 库都放到代码库下一个目录中(比如 mypackage/vendor)以外,还需要修改所有的 import 语句,指向到这个目录中。 比如,把 import requests 改成 from mypackage.vendor import requests。PDM 中也包含了这样一个目录,我是使用和 pip 相同的工具来管理 vendor 的。 这个工具是 vendoring,文档很少(因为就没人要用)。它包含以下几个功能: 读取一个 requirements.txt 下载依赖到指定目录 下载所有库的 LICENSE 文件到这个目录中 从一个指定路径读取 patch 文件并应用到源代码中 重写所有 import 语句,指向到 vendor 目录中 更新 vendor 的版本 使用过程,也大致按上面的步骤。首先建立一个 mypackage/vendor 目录,在其中创建一个 vendors.txt,填写依赖(requirements.txt 格式): requests==2.24.1 click==8.0.1 然后在项目根路径下的 pyproject.toml 中,添加以下内容: [tool.vendoring] destination = "mypackage/vendor/" # vendor目录路径 requirements = "mypackage/vendor/vendors.txt" # requirements路径 namespace = "mypackage.vendor" # import 重命名前缀 protected-files = [ "__init__.py", "README.md", "vendors.txt" ] # 每次重新 vendor 时需要保留的文件 patches-dir = "tasks/patches" # patch 文件目录 [tool.vendoring.transformations] substitute = [ # 重命名没有覆盖到的 import,文件替换规则 { match = '__import__("requests")', replace = '__import__("mypackage.vendor.requests")' } ] drop = [ # 需要从 vendor 库中去除的文件 "bin/", "*.so", "typing.*", "*/tests/" ] 最后运行 vendoring sync,就会自动把 vendor 全部准备好了。 对于 patch 文件,其实就是 git diff 的输出,有了这个文件,git 就可以从源代码重新建立 vendor 目录。生成方法: 配置好以后跑一次 vendoring sync,把文件提交到本地仓库(只 commit 不 push) 修改源代码 运行 git diff --patch <file_path> > <patches_dir>/<file_name>.patch,把 patch 文件保存到 patches_dir 中 审核 patch 文件,把其中已经被修改的 import 语句恢复成原始的 import 语句,比如 from mypackage.vendor import requests 改成 import requests[^1] 运行 git add . && git commit --amend,提交修改 再次运行 vendoring sync 验证一下,如果一切正常,应该不会产生任何变更,说明这个 vendor 过程是 reproducible 的 [^1]: 至于为何要这么做,因为 apply patch 是先于 import 重写的,所以 patch 文件中,应该都是未重写的 import 语句。修改时要注意不要改动任何空白字符,patch 文件对空白是敏感的。

来自 Frost's Blog作者主页
在原博客阅读全文 (新标签页打开)

以上内容来自博客订阅源,版权归原作者。关注作者后可以在 关注列表 第一时间查看作者文章。

一起聊聊 0

尊重不同观点

还没有评论。欢迎分享第一个想法。

RSS SUBSCRIPTIONS

选择 RSS 订阅

把博客汇的文章添加到你常用的阅读器。

关注文章 RSS 订阅本订阅需要登录后使用