防止私有和基于 pypi 的 Python 包之间的命名空间冲突

Posted

技术标签:

【中文标题】防止私有和基于 pypi 的 Python 包之间的命名空间冲突【英文标题】:Preventing namespace collisions between private and pypi-based Python packages 【发布时间】:2020-11-28 21:42:18 【问题描述】:

我们有 100 多个私有包,到目前为止,我们一直在使用 s3pypi 在 s3 存储桶中设置私有 pypi。我们的私有包相互依赖(以及公共包),我们的 GitLab 管道找到它所依赖的包的最新功能版本(当然)很重要。 IE。我们对最新签入的代码不感兴趣。我们仅在测试后才创建新***,并且 qa 已与推动掌握(这是解释-e <vcs> 要求不起作用的冗长方式)。

我们的设置运行良好,直到有人在官方 pypi 上创建了一个新的公共包,它隐藏了我们的一个包名称。我们可以通过增加版本号来强制选择我们的私有包,使其高于 pypi.org 上的新包 - 或者将我们的包重命名为尚未在 pypi.org 上使用的包。

这显然是一个 hacky 和脆弱的解决方案,但显然功能是这样的 by-design。

在初始存储桶设置后,s3pypi 无需维护或管理。上面的票建议使用devpi,但这似乎是一个非常繁重的解决方案,需要管理/监控/等。

GitLab 的 pypi 解决方案似乎是在单个包级别(这意味着我们必须列出多达 100 个以上的 url - 每个包一个)。这似乎不切实际,但也许我不明白一些东西(我也可以看到我们组下的包注册表菜单,但文档指向“package-pypi”文档)。

我们不可能是第一个遇到这个问题的小公司..?有没有比在 pypi.org 上注册我们所有包的虚拟版本更好的方法(版本=0.0.1,所以首选 s3pypi 版本)?

【问题讨论】:

这可能为时已晚,但我建议您在私有包名称前加上前缀,例如yourcompanyname-packagename. @DustinIngram 是的,有点晚了 :-) 我们确实为一半多一点的包添加了前缀,尽管前缀较短,所以可安装和可导入的名称是相同的——因为它使我们的代码使用包作为数据(跨包依赖分析等)更容易。 您可以使用requirements.txt 文件,然后为每个包指定索引,如***.com/a/61784078/5666087。 @jakub 将--index-url 声明添加到requirements.txt 的问题是指定的url 将是唯一用于查找的,即如果我的包foo 在setup.py/install_requires pip 中有PIL将尝试在我的私人仓库中查找 PIL,然后失败... 我们在最近的两个工作中解决了类似的问题,使用 Artifactory 和 Sonartype,为公共的创建一个代理包存储库,为内部的东西创建一个私有包存储库,然后公开一个虚拟包聚合这两者的存储库。我们将我们的包上传到私有包,并始终从虚拟包中查询/安装它们。你请求一个包,它首先在 private 中查找,如果没有找到,代理会尝试 public pypi。 【参考方案1】:

@a_guest 对我的第一个答案的评论让我开始思考,“问题”是 pip 在对候选人进行分类以满足要求时没有考虑包的来源。

所以这里有一种可能的方法来改变它:Monkey-patch pip 并引入对索引的偏好。

from __future__ import absolute_import
import os
import sys

import pip
from pip._internal.index.package_finder import CandidateEvaluator


class MyCandidateEvaluator(CandidateEvaluator):
    def _sort_key(self, candidate):
        (has_allowed_hash, yank_value, binary_preference, candidate.version,
         build_tag, pri) = super()._sort_key(candidate)

        priority_index = "localhost"  #use your s3pipy here
        if priority_index in candidate.link.comes_from:
            priority = 1
        else:
            priority = 0

        return (has_allowed_hash, yank_value, binary_preference, priority,
                candidate.version, build_tag, pri)


pip._internal.index.package_finder.CandidateEvaluator = MyCandidateEvaluator

# Remove '' and current working directory from the first entry
# of sys.path, if present to avoid using current directory
# in pip commands check, freeze, install, list and show,
# when invoked as python -m pip <command>
if sys.path[0] in ('', os.getcwd()):
    sys.path.pop(0)

# If we are running from a wheel, add the wheel to sys.path
# This allows the usage python pip-*.whl/pip install pip-*.whl
if __package__ == '':
    # __file__ is pip-*.whl/pip/__main__.py
    # first dirname call strips of '/__main__.py', second strips off '/pip'
    # Resulting path is the name of the wheel itself
    # Add that to sys.path so we can import pip
    path = os.path.dirname(os.path.dirname(__file__))
    sys.path.insert(0, path)

from pip._internal.cli.main import main as _main  # isort:skip # noqa


if __name__ == '__main__':
    sys.exit(_main())

设置requirements.txt

numpy
sampleproject

并使用与 pip 相同的参数调用上述脚本。

>python mypip.py install --no-cache --extra-index http://localhost:8000 -r requirements.txt
Looking in indexes: https://pypi.org/simple, http://localhost:8000
Collecting numpy
  Downloading numpy-1.19.1-cp37-cp37m-win_amd64.whl (12.9 MB)
     |████████████████████████████████| 12.9 MB 6.8 MB/s
Collecting sampleproject
  Downloading http://localhost:8000/sampleproject/sampleproject-0.5.0-py2.py3-none-any.whl (4.3 kB)
Collecting peppercorn
  Downloading peppercorn-0.6-py3-none-any.whl (4.8 kB)
Installing collected packages: numpy, peppercorn, sampleproject
Successfully installed numpy-1.19.1 peppercorn-0.6 sampleproject-0.5.0

将此与默认 pip 调用进行比较

>pip install --no-cache --extra-index http://localhost:8000 -r requirements.txt
Looking in indexes: https://pypi.org/simple, http://localhost:8000
Collecting numpy
  Downloading numpy-1.19.1-cp37-cp37m-win_amd64.whl (12.9 MB)
     |████████████████████████████████| 12.9 MB 6.4 MB/s
Collecting sampleproject
  Downloading sampleproject-2.0.0-py3-none-any.whl (4.2 kB)
Collecting peppercorn
  Downloading peppercorn-0.6-py3-none-any.whl (4.8 kB)
Installing collected packages: numpy, peppercorn, sampleproject
Successfully installed numpy-1.19.1 peppercorn-0.6 sampleproject-2.0.0

注意mypip 更喜欢可以从localhost 检索到的包;你可以进一步自定义此行为。

【讨论】:

这依赖于 pip 的内部(私有)API,因此它不是一个稳定的解决方案。您必须控制 pip 的每次更新,以确保相关部分仍然存在。 这显然不是几乎所有情况的“正确”答案,但我喜欢创造力和“如果其他一切都失败,请使用更大的锤子”的方法 - 你会得到赏金。特别感谢@a_guest 的鼓励 ;-) 在其理想中似乎非常合理;也许制作一个PR to upstream!如果另一个 hack 没问题,也可以使用 inspect to make sure the text of CandidateEvaluator hasn't changed【参考方案2】:

您或许可以从一个 requirements.txt 和两个 pip 调用中获得您正在寻找的行为:

cat requirements.txt | xargs -n 1 pip install -i <your-s3pipy>
pip install -r requirements.txt

第一个尝试从本地存储库安装它可以安装的内容,如果失败则忽略一个包。第二个调用尝试从 pipy 安装之前失败的所有内容。

这是因为--upgrade-strategy only-if-needed 是默认值(我相信从 pip 10.X 开始,不要引用我的话)。如果您使用的是旧点子,则可能必须手动指定。


这种方法的一个限制是,如果您期望/请求一个本地包,但它不存在并且 pipy 上存在同名的包。在这种情况下,您将获得该软件包。不确定这是否是一个问题。

【讨论】:

如果私有发行版依赖于托管在 pypi 上的公共发行版怎么办?然后它不会在第一个命令期间安装,因为它无法解决该依赖关系,它将在第二个命令期间安装,其中出现名称冲突的相同问题。 @a_guest 在这种情况下,您会真的想要使用命名空间。您可以编写一个通过pip --no-deps -i &lt;private-s3pipy&gt; dist 安装分发的脚本。如果失败,它会将分发添加到要从 pipy 安装的列表中;如果成功,它将递归到分发的要求并继续。部分安装会出现问题,但如果您无法解析软件包、具有循环依赖关系、无法满足某个版本……。在这一点上,感觉就像你只是在运行自己的依赖管理器。命名空间似乎更容易...... “如果成功,它会递归到分发的要求中并继续” 您将如何实现这一点?据我所知pip 没有--only-deps 选项,即使有,您也可能需要原始pypi 索引;但在这种情况下,名称冲突仍然是一个问题。问题是 pip 对所有索引给予同等优先级,但这并不总是可取的。 @a_guest 有pip check,它会检查您是否缺少依赖项或依赖项冲突。它为每个缺失的 dep 打印一个文本;您可以解析它或查看源代码并调用内部pip._internal.operations.check.check_package_set,它会返回missingconflicting 依赖项列表,然后您可以对其进行迭代和解析。不过,您必须为此提供逻辑。 老实说,这听起来更容易滚动你自己的 pip 分支,它实现了包索引的搜索顺序。虽然它看起来确实可行。我认为您应该将其添加到您的答案中,因为就目前而言,它不会解决 OP 的问题。【参考方案3】:

我们为此使用 VCS。我看到您已经明确排除了这一点,但是您是否考虑过使用分支来标记您在 VCS 中的最新稳定版本?

如果您对最新版本的 master 或 dev 分支不感兴趣,但您正在针对提交运行 test/QA,那么我会将您的 test/QA 套件配置为合并到一个名为“stable”之类的分支中或“pypi-stable”,然后您的需求文件如下所示:

pip install git+https://gitlab.com/yourorg/yourpackage.git@pypi-stable

相同的配置将适用于 setup.py 需求块(允许链接的内部依赖项)。

我错过了什么吗?

【讨论】:

啊,“使用this one”策略。我喜欢它的简单。我没有想过要像这样使用分行作为存放处。嗯...我想我们可以使用类似的策略,只需将轮文件复制到 s3(而不是将其上传到类似 pypi 的结构)并在 requirements.txt 文件中使用https://s3.bucket/dev-wheels/foo.whl...(可能更快在“上传”时合并到一个分支,但下载+安装***可能比克隆更快——尽管我们可能需要通过 --no-cache-dir 绕过***缓存...)【参考方案4】:

您的公司可以将所有对 pypi 的请求重定向到您首先控制的服务(可能只是在您的构建服务器的 hosts 文件中)

这可能会让你

使用本地包优先/覆盖任意包 检测此类情况 在本地缓存常见/大型上游包 拒绝upstream packages 的可疑/未知版本/名称

【讨论】:

这就是我们想要做的。 s3pypi 包/工具在您控制的 S3 存储桶中创建一个 pypi 索引,我们在 PIP_EXTRA_INDEX_URL 环境变量中指定此存储桶的 url,以便 pip 知道它。 pip 将选择我们存储桶中的版本,只要它是 (i) 唯一命名的,或者 (ii) 版本号高于官方 pypi 中的版本(两者都不能依赖)。我更喜欢不涉及编写我们自己的 pypi 版本的解决方案;-) @thebjorn 我认为 ti7 的意思是您可以尝试配置服务器的 DNS 设置,使其将pypi.org 解析为your-bucket.url(例如通过ALIAS),然后设置PIP_EXTRA_INDEX_URL=pypi-orig.org这反过来又被解析为原始的pypi.org。这只是一个草图,我不知道 S3 是否可行。或者你可以让它指向一个自定义服务,该服务管理对 pypi 和你的私有索引的调度。 @a_guest 我们没有尝试弄乱 DNS 记录,但是仅仅反转哪个站点被认为是官方/额外索引(可以使用 env-vars 完成)是行不通的。如果在两个地方都找到了包,pip 仍会查找最高版本号。 @a_guest 当然,那么我只需要编写自定义服务,并在我们创建新包时更新它关于哪些包是我们的信息(并为其分配服务器空间,附加监控和警报, ..以及生产服务器附带的所有其他“东西”)。我们是一家小公司,所以我希望能有更多交钥匙的东西——我们不可能是第一个遇到这些问题的小公司..? @thebjorn 再想一想,将版本号增加 1000(或者你说的更小的数字)并不完全安全,因为在 pypi 上新注册的项目可能使用date-based versioning scheme因此使用像2020.8 这样的版本号,它很可能比任何语义版本号都大。【参考方案5】:

这可能不是适合您的解决方案,但我告诉我们该怎么做。

    为包名添加前缀,并使用命名空间(例如company.product.tool)。 当我们安装我们的包(包括它们的内部依赖项)时,我们使用一个包含我们的 PyPI URL 的requirements.txt 文件。我们在容器中运行所有内容,并在构建映像时在其中安装所有公共依赖项。

【讨论】:

命名空间是一个很棒的主意——让我们做更多的事情! - Python 的禅宗,Tim Peters python -c 'import this'

以上是关于防止私有和基于 pypi 的 Python 包之间的命名空间冲突的主要内容,如果未能解决你的问题,请参考以下文章

设置私有 pypi 包?

搭建使用与维护私有PyPi仓库

即使在名称冲突的情况下,pip 也可以从 PyPi 上的私有索引中选择包?

Python 进阶 — 创建本地 PyPI 仓库

详细介绍去一年在 PyPI 上下载次数最多的 Python 包

重定向 pip、setuptools 以及与私有 PyPI 存储库相关的所有内容