首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PDM - 一款新的 Python 包管理器

PDM - 一款新的 Python 包管理器

作者头像
岂不美哉Frost
发布2020-03-02 14:21:02
1.3K0
发布2020-03-02 14:21:02
举报
文章被收录于专栏:Frost's BlogFrost's Blog

去年临近跨年的某一天,一个包管理器突然在脑海中形成了蓝图。粗略地估计了一下我的编码能力,我认为这在我的能力范围之内,于是尽管年底非常忙,还要忙着晋升答辩的事情,我还是腾出空(摸鱼)写下了我的第一行代码。

image-20200228135957046
image-20200228135957046

这个项目就是pdm,我给它取了一个很装逼的名字——Python Development Master。截止发文时,已经在PyPI上发布了0.3.0版本,它包含以下特性:

  • PEP 582 本地项目库目录,支持安装与运行命令,完全不需要虚拟环境
  • 一个简单且相对快速的依赖解析器,特别是对于大的二进制包发布。
  • 兼容 PEP 517 的构建后端,用于构建发布包(源码格式与 wheel 格式)

做一个项目,首先自己要用起来,至少对我来说,这些功能非常Exciting,而且我随时可以根据自己的喜欢做新功能(P.S. 是的,当Pipenv的维护人却没有什么权限发布新版这太让人沮丧了)。如果你对这个新工具也感兴趣,可以访问官方文档或是GitHub主页。

不如就多说一些别的吧,当做是我开发这个项目的碎碎念。

把握造轮子的程度

造轮子造轮子,造法也有很多种,你可以从零件厂采购轮毂,轮胎,自己组装,也可以从冶金、找橡胶树资源开始。

1. 整体引用

前一种方法,省事,相当于你只把内部的组件打乱重组,包装成一个新的样子出来。Pipenv即属此类,它其实是由pip(安装器),virtualenv(虚拟环境),pip-tools(依赖解析)几大部分组合而成,连接调度的方式居然是通过subprocess call,所以这里面子进程启动、输出结果解析,都是耗时的。其余的次要组件,包括依赖树显示、依赖安全性检查等,无一例外都是通过内嵌别的库实现的。这种方法,很懒,引用作者Kenneth Reitz本人的话,叫做I (re)design beautiful APIs。其中一大缺点,就是要做什么bug修复、feature引入,非常依赖上游库的更新,要不就是有很重的vendor系统,非常不自由。

比如我要安装一个包,用这种方法实现出来是这个样子:

PYTHON

def install_requirement(requirement):
    # requirement是符合PEP508规范的依赖格式
    subprocess.check_call([pip_path, "install", "--no-deps", requirement])

2. 使用内部API

当你对于上游库的修改多到了一定程度,你一气之下,决定化整为零,把依赖的库拆散,只取它内部的结构和接口来做。还是同样的功能,用pip内部的API实现起来,是这个样子的:

PYTHON

def install_requirement(requirement):
    from pip._internal.req.constructors import install_req_from_line
    
    ireq = install_req_from_line(requirement)
    ireq.install(["--no-deps"])

看看那一串长长的import string,人家都叫做internal还带下划线了,都挡不住你要从里面import。这就是这种方法的问题所在了:不稳定。可能下个版本,pip就升级了,API会变得完全不同,那么你就要做相应的改变。

3. 自己动手,丰衣足食

你改了几个版本以后,心里暗骂了一句pip的祖宗,责怪它为什么老改API,一怒之下全部推倒重来,不求别人,全都自己实现,于是这一版代码变成了这样:

PYTHON

def install_requirement(requirement):
    req = parse_install_requirement(requirement)
    download_artifact(req)
    if not req.is_wheel:
        unpack_artifact(req)
        build_wheel(req)
    copy_modules(req)
    install_scripts(req)

我省略了很多代码,这里的每一个函数,背后都是几十上百行的代码,因为requirement的类型是很多的,有本地的文件、有Git的地址,有的带marker,有的带extras……你要覆盖到这所有的情况,难免出bug。这时就体现出用第三方库的优点来了:它们可能已经帮你把所有的bug都踩过了,并受过生产环境中的考验。

所以造轮子要用哪种方法来造,是要经过仔细的考量的。用了第一种,结果发现需要定制很多;用了第三种方法,结果发现天天都在修bug。我在开发PDM初始,基于对个人精力的评估,选择的是第二种方法,尽管我有一万次想丢掉pip这个包袱。

选择合适的mock策略

测试的时候往往会依赖一些外部的服务,而这些外部服务有可能1)不可用;2)和你的代码正确性无关。这种情况下就需要mock技术,测试和mock说起来话就长了,够写一本书,我就挑一个具体的场景来说。

测试一个包管理器,PyPI是一个最重要的外部服务。Pipenv使用的技术是模拟了一个本地的PyPI服务器,实现了PyPI的接口,然后把所有指向PyPI的请求改为本地的PyPI服务。这种方法对测试代码的侵入是非常小的,你甚至只需要修改PyPI的URL为https://127.0.0.1:{port}/simple就可以了。但这依然要求服务器上的文件在本地也有。这会带来额外的负担,也会拖慢测试执行的速度。现在Pipenv跑一遍完整的测试需要45 min,请问谁受得了?

image-20200228145339946
image-20200228145339946

如上图所示,find_matches(requirement)的作用是根据给定的依赖去PyPI上寻找符合条件的安装包。它会去PyPI的/simple/<package>端点获取所有链接地址,然后封装成对象返回。其实测试这个接口,并不需要一个PyPI服务器,更不需要真实的安装包文件,你只需要保证返回的结果里包含你想要的数据即可。所以PDM拦截了这个请求,转而从一个JSON文件中取数据返回。这大大的加快了测试的速度。

又比如,我要测试一个获取远程文件的接口,它通过requests.get(url)去获取文件内容并下载到本地。测试时完全可以把这个文件放在本地,然后请求时读取这个文件内容即可,下面是一个把requests请求mock掉的方法:

PYTHON

class LocalFileAdapter(requests.adapters.BaseAdapter):
    def __init__(self, base_path):
        super().__init__()
        self.base_path = base_path
        self._opened_files = []

    def send(
        self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
    ):
        file_path = self.base_path / urlparse(request.url).path.lstrip(
            "/"
        )  # type: Path
        response = requests.models.Response()
        response.request = request
        if not file_path.exists():
            response.status_code = 404
            response.reason = "Not Found"
            response.raw = BytesIO(b"Not Found")
        else:
            response.status_code = 200
            response.reason = "OK"
            response.raw = file_path.open("rb")
        self._opened_files.append(response.raw)
        return response

    def close(self):
        for fp in self._opened_files:
            fp.close()
        self._opened_files.clear()
        
# 使用
session = requests.Session()
session.mount('http://fake-url.org', LocalFileAdapter(Path('./fixtures')))

这个例子中是通过mock Adapter.send()方法实现的,这是一个相当底层的接口,所有经过http://fake-url.org的请求都一定会通过这个方法。你当然可以通过mockresponse.get_content()实现相同的效果,但你无法保证上游调用者一定是调用这个方法。mock得越底层,能使得你的mock适用范围越广。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-02-28T,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 把握造轮子的程度
    • 1. 整体引用
      • 2. 使用内部API
        • 3. 自己动手,丰衣足食
        • 选择合适的mock策略
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档