[译] 将一个旧的大型项目迁移到 Python 3

将一个旧的大型项目迁移到 Python 3

一年半前,咱们就决定使用 Python 3 了。咱们已经讨论了很长时间,如今是时候使用了!如今这个过程已经结束了,咱们已经把生产环境的最后部署都迁移到了 Python 3html

  • 整个代码库大约有 240 k 行,不包括空行和注解。
  • 这是一个基于 Web 的批处理任务系统。而且只有一个生产,部署环境。
  • 代码库大约有 15 年的历史了。
  • 虽然这是一个 Django 应用程序,但部分代码是先于 Django 公布以前写的。

关于修改 Python 3 的一些基本统计数据,是基于对 git 提交历史的粗略过滤产生的:前端

  • 275 次提交
  • 4080 次添加代码行
  • 3432 次删除代码行

我发现有 109 个 jira 问题与这个项目相关。python

Py2 → six → py3

咱们的理念一直是 py2 →py2/py3 → py3 由于咱们实在没法在实际生产中实现巨变,这种直觉也以使人惊讶的方式被证实是正确的。这意味着 2 到 3 是不可能的,我认为这很常见。咱们尝试过使用 2 to 3 来检测 Python 3 的兼容性问题,但很快这也被发现没法成立。基本上,这样的更改意味着在 Python 2 中的代码将被破坏。这样的改变不可行。android

结论是使用 six, 这是一个库,能够方便的构建一个在 Python 2 和 3 中都有效的代码库。ios

首当其冲的就是更新以前的依赖关系。这项工做须要马上启动,由于以后会有更多的内容要更新。git

现代化

Python-modernize 是咱们选择进行迁移的工具。它是一个能够自动将 Py 2 代码库转换为可兼容 six 代码库的工具。咱们首先引入一个测试,做为 CI 的一部分,来检查基于 modernize 的新代码是否已经准备好兼容 py3 了。这样作最大的效果的是让那些仍使用 Py 2 语法的人意识到新的处理方法,但这显然对将现有的 240 k 行代码转化到 six 做用不大。咱们都有使用旧语法的坏习惯,这能够说是教学上的成功了,即便它对代码行的计数没有什么不一样,它也被咱们用于实验分支:github

实验分支

我新建了一个名为“Python 3 ”的分支,并作了如下操做:数据库

  • 在整个代码库上运行“python-modernize -n -w” 。它会在合适的地方修改代码。我常常作完这步后没有进行第一次提交就开始修复代码。这个错误步骤老是让我后悔,不止一次地迫使我从新开始作整件事情。即便这个阶段出错,最好仍是先把它提交。所以将机器和人要作的事情分开显得尤其重要。
  • 将全部用于函数体的依赖项导入到咱们尚未修复的 py3。

这里的想法是“run ahead”,即看看若是咱们没有使用过期的依赖项,咱们会遇到什么问题。这个分支容许我在超级中断状态下能够很是快速地启动应用程序,至少能够运行一些单元测试。 这个分支有很大的不一样,但我仍是找到了把它应用在适当场景的方法。我使用优秀的 GitUp 来拆分、组合和提交。当一个提交看起来不错的时候,我会把它挑选到一个新的分支,而后发给代码审查。后端

没有人能够在这个分支上工做,由于它被不断地 rebase ,强制推送,滥用,可是它确实让项目向前推动了,而不用等待全部的依赖项被更新。我强烈推荐使用这种方法!函数

静态分析

咱们添加了预提交钩子,因此若是您编辑了一个文件,就会收到建议将 Python 3 所有进行 modernize 更新的提示。

quote_plus 的手动静态分析: 在处理 quote_plus 和 six 上有一些细微差异。最后,咱们建立了本身的包装器,默认代码强制执行使用这个包装器,而不是使用标准库中的包装器,也不使用 six 中包装器。咱们还静态检查了您从未给 quote_plus 发送过的字节。

咱们修复了每一个 diango 应用程序中全部的 python 3 问题,并在 CI 环境中使用一个白名单强制执行了这一点,因此您没法破坏一个曾经修复过的应用程序。

依赖

对于咱们来讲,解决依赖是最困难的部分。咱们有不少依赖,因此花了不少时间,其中有两个依赖关系比较棘手:

  • splunk-lib. 咱们依赖于 splunk,可是直到今天,他们仍然忽略全部要求为客户端增长 py3 兼容性的愤怒的客户。咱们团队中的一我的 最后本身亲自动手来解决这个问题。Splunk 处理得真的很糟糕,它甚至把这个评论区的这个问题锁上了!这简直让人没法接受。
  • Cassandra. 咱们的整个产品都在使用这个数据库,可是咱们使用了一个有之前 API 模块的旧的驱动程序。对于咱们来讲,py3 的迁移过程当中,这占据了很大的一部分,所以咱们必须逐段重写全部的这些代码。

测试

咱们的代码测试覆盖率大约有 65% 包括:单元、集成, 以及 UI 合并。 咱们确实编写了更多的测试,但整体数量并无发生太大的变化。考虑将覆盖率从 65% 提升到 66% ,意味着编写将近2000 行代码的测试,这一点也不奇怪。

咱们必须跳过须要 Cassandra 的测试,同时修复这个依赖项。 我发明了一个有趣的小 hack 来使它发挥做用, 并写了这方面的文章.

代码更改

关于代码更改的说明,在如何将 py2 迁移到 six 的文档中并未说起 (也许是咱们错过了):

StringIO

咱们在代码中大量使用 StringIO 。第一反应就是使用 six。但对于 StringIO 来讲,这在几乎全部状况下 (但不是所有!)都被证实是错。基本上,咱们必须很是仔细地考虑每个咱们使用 StringIO 的地方,并试图弄清楚咱们是否应该用 io.StringIO, io.BytesIO 或者 six.StringIO 来替代它。这里犯错的表现一般为看起来像兼容 py3 的代码准备好了,在 py2 中能够正常运行,却实际上在 py3 中是失效的。

future 中导入unicode_literals

这是一件好坏参半的事情。您能够经过将它添加到许多文件中来发现 bug,可是有时会在 py2 中引入 bug。 当日志忽然在奇怪的地方,好比在字符串前写"u"时,它也会变得使人困扰。总的来讲,这显然不是我所指望的效果。

str/bytes/unicode

这在很大程度上是您所指望的。我感到惊讶的是,在 py2 和 py3 中须要 str 。若是未来您使用 unicode_literals 导入,那么一些字符串须要从 'foo' 修改成 str('foo')

six.moves

six.moves 的实现是一个很是奇怪的黑客行为,所以它不像它伪装的普通 Python 模块那样运行。 我也不一样意他们在 six.moves 中不包含 mock 的选择。咱们必须使用他们的 API 来本身添加它,但这让咱们很难开始工做,并且它要求咱们将 from mock import patch 改成 from six.moves import mock 这也意味着 patch 如今变成了 mock.patch

CSV 的解析是不一样的

若是你使用 csv 模块,你须要了解 csv342。在我看来,这应该是 six 的一部分。不然就意味着你没有意识到有问题。不过咱们在许多地方都没有使用 csv342,因此您这里要作的工做可能会有所不一样。

发布顺序

咱们首先进行测试:

  • 在 CI 中进行单元测试
  • 在 CI 中进行集成和UI测试(不包括 Cassandra)
  • 在 CI 中进行 Cassandra 测试 (这要晚于以前的步骤!)

接下来就是产品自己了。咱们创建一台拥有能一次性切换到 py3 的能力的批处理机器,而且相当重要地是将其切换回来。当在 py3 上发生中断时,这一点就显得很重要了。这对咱们来讲是很好的,由于咱们能够从新排队那些中断的任务,可是咱们不能中断太多或者任何其实是很关键的任务。咱们使用 Sentry 来收集奔溃日志,因此很容易查看迁移到 py3 时遇到的全部问题,并且当咱们修复了全部的问题时,咱们须要再次迁移到 py3,直到咱们获得一些问题,如此反复。

咱们有以下环境:

  • Devtest: 开发人员在内部使用,因此大多数状况下,这只是用来测试数据库迁移。这个环境很是容易使用,因此这里不常常出问题。
  • IAT (内部验收测试):用于验证更改,并在咱们将更改推送到生产以前执行回归测试。
  • UAT (用户接受度测试): 客户能够访问的测试环境。用于须要准备客户系统的变动,或者让客户在上线前查看变动。这个环境在数据库迁移前几天才会迁移。
  • 生产环境

咱们按照如下顺序将 Python 3 发布到这些环境中:

  • Devtest 环境
  • 短时间 IAT 环境
  • 长期 IAT 环境
  • 一台短时间的批处理生产机器
  • 在工做期间使用的一台批处理生产机器
  • 生产 SFTP
  • 占一半生产的批处理机器
  • 生产批次
  • 生产 Web (在测试环境的长时间手动测试运行以后)
  • 生产负载机器。这是批处理的一个特殊子集。它完成了咱们产品中 CUP 和内存最多的部分。

负载机器暴露了与 Python 3 不兼容的客户数据配置,所以咱们必须在 Python 2 中实现对这些状况的警告,并确保再次打开 Python 3 以前已经修复了它们。这花了几天时间,由于咱们天天都会收到客户数据,因此每次都会有一个警告,这又让咱们不得再也不等一天。

生产中的惊喜

  • 'ß'.upper() 在 py2 中是  'ß' 可是在 py3 中是  'SS' 。当产品的最后一部分迁移到 py3 时,最终致使了产品的崩溃!
  • 在 py2 中对不一样类型的对象进行比较和排序是有效的,但这隐藏了大量的 bug 。咱们获得了一些使人讨厌的惊喜,由于这种行为以一些不明显的方式从堆栈中泄露出来,特别是在一些排序列表中存在  None 的时候。总的来讲,这是一个胜利,由于咱们发现了至关多的 bug 。 None 在 py2 的列表中排在第一位,这可能会让人感到惊讶(您可能会指望它被排序到接近于零的地方!), 如今咱们只须要来处理它们。
  • '{}'.format(b'asd') 在 Python 2 中是 'asd' , 可是在 Python 3 中是 "b'asd'" 。在 Python 3 中,这里几乎任何其余行为都会更好: 输出为十六进制 ( 结果明显更不同 ) ,旧的行为 (以前的代码运行),或者抛出异常 (最好的行为!)。
  • int('1_0') 在 py 3 中结果是 10 , 可是在 py2 中无效。这甚至在切换到 py3 以前就困扰了咱们。由于这种错配致使了另外一个在咱们以前使用 py3 的团队给咱们发送了咱们认为无效而他们认为有效的有效值。我我的认为这个决定是错误的:很是严格的解析是更好的默认方式,我担忧这将在将来几年会继续以微妙的方式困扰咱们。

结论

最后,咱们以为在这件事上咱们真的别无选择: Python 2 的维护将在某个时刻中止,咱们的依赖项仅限于 py3,最明显的就是 Django。可是,不管如何,咱们仍是想要进行这种转换,由于咱们常常会被 bytes/Unicode 问题困扰,而且Python 3 仅仅是修复了 Python 2 中的许多小麻烦。此次迁移过程,咱们已经在生产过程当中发现了一些实际的漏洞/错误配置。咱们也期待在任何地方均可以使用 f-string 和有序字典。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索