在我看来,代码质量就是程序员的职业底线。维护底线不能全靠自觉,因此本文将介绍几种自动化的工具,并展示如何将它们集成到日常的工作流中,省心省力的持续保障代码质量。
Linter
Linter 是一类用于标记程序错误、bug、风格错误和可疑结构的静态代码分析工具,比如 Python 中的 Pylint
、 flake8
,JavaScript 中的 EsLint
,这类工具会找到你代码中的错误,并给出如何修复的提示。
为何要使用 Linter
人总是会粗心和犯错,借助 Linter 我们可以发现代码中不易察觉或遗漏的错误,并进行纠正。使用 Linter 有以下好处:
- 根据 PEP8 等语言规范持续的提示与纠正,帮助你写出更好的代码
- 减少代码中的格式错误、笔误及糟糕的风格等低级错误
- 节省团队成员 review 你代码的时间,提高协作效率
- 配置简单,仅需几步即可上手
flake8
使用示例
接下来我们以 Python 中的 flake8
为例,介绍其用法。
首先通过 pip
安装 flake8
:
|
|
flake8
的使用非常简单,直接以需要检查的文件或整个目录的路径作为 flake8
命令的参数:
|
|
在运行结果中, flake8
不仅指出了具体的错误原因,还给出了出错的文件位置及代码所在行数。
更多其他的选项可通过以下命令获取帮助:
|
|
flake8
的可自定义程度很高,除了在运行时添加命令选项,我们还可以通过配置文件来自定义检查规则。方法是在运行 flake8
的项目根目录添加 .flake8
文件,并写入如下内容:
|
|
flake8
还可以添加插件实现更多更强大的功能,比如我们想强制要求编写函数 docstring
,可以安装 flake8-docstrings
:
|
|
重新运行检查后,会发现结果中多出了如下错误:
|
|
更多其他功能和插件可查看 flake8
的文档。
除了 flake8
以外,Python 还有以下 Linter 可供选择,它们的用法大同小异,但在检查范围、容忍度等方面有所区别:
Static Type Checker
Python 从 PEP 484 引入 Type Hints
(类型提示)以来,配套的工具链生态也在逐渐成熟,出现了如 mypy
、 pyright
等静态类型检查工具。
它们可以根据 Python 代码中的类型提示进行静态类型检查,不需要运行程序就可以找到程序中的错误。而且如果有遗留代码不好处理,还可以在程序中混合使用动态和静态类型。
我们以 mypy
做简单示例,假设我们有以下代码:
|
|
通过 pip
安装 mypy
:
|
|
对该文件运行检查:
|
|
在检查结果中, mypy
提示我们在调用 greeting()
时传入的参数类型出错。
类型提示带来的好处
静态类型检查工具可以帮助我们像静态语言一样在运行代码之前就捕获到某些错误,但需要我们在程序中加入大量的类型提示才能完整发挥其作用。虽然稍微加大了工作量,但引入类型提示还会带来以下好处:
- 相比传统的 docstrings,类型提示配合良好的命名既能作为解释文档,也能用于自动检查。
- 可以使 IDE 通过类型推断提供更好的代码补全和提示功能。
- 强制你去思考动态语言程序的类型可能会帮助你构建更清晰的代码架构。
Autoformatter
Autoformatter (自动格式化器)顾名思义是可以将代码按特定规则自动格式化的一类工具。Linter 可以帮助我们发现代码中的格式错误和风格问题,但并不会自动纠正,因此我们还需要借助自动格式化器,进一步将我们从重复琐碎的手动修改中解放出来。
Python 中有 black
、 yapf
、 autopep8
、 isort
等众多格式化工具,除了整体上遵循 PEP8 以外,各自都有不同的风格规范和适用范围,我们可以根据自己的喜好进行选择。
以 black
和下面这段代码为例:
|
|
首先通过 pip
安装 black
:
|
|
然后对需要格式化的文件或整个目录的路径执行 black
命令:
|
|
black
会首先检查出不符合其规范的文件,然后编辑文件完成修改,并输出修改的文件路径及数量。格式化后的文件内容如下:
|
|
black
号称「不妥协的代码格式化器」,其不妥协体现在基本没有可供自定义的选项,要么全盘接受它的代码风格,要么就不用它。如果你或者你的团队能够接受它的风格,使用 black
可以节省很多花在代码风格上的精力和时间(再也不用争论某种风格孰优孰劣了)。
实践中我们可以对最大行长度以及引号格式化选项做一些调整,方法是在执行 black
的项目根目录添加 pyproject.tomal
文件,并写入以下内容:
|
|
IDE 的自带工具
现代的 IDE 和编辑器基本都会自带 Linter 和格式化等功能,比如 PyCharm
就可以通过 ⌥ ⌘ L
快捷键格式化当前文件的代码,这样的话我们还有必要手动集成这些第三方工具吗?答案当然是有必要,我们以第三方和 IDE 自带的代码工具作对比:
- 自带工具对环境依赖程度很高,必须要有特定的二进制包及配置文件才能执行;而使用与项目相同语言实现的第三方工具,可以直接作为项目的开发依赖,配置也能很方便地整合到项目本身的配置文件中(如
pyproject.toml
、tox.ini
等),且通常都可以很快地集成到不同 IDE 中。 - 自带工具只能在 IDE 的图形界面中手动执行;而第三方工具只要有基础的代码运行环境就可以执行,因此我们可以把它们很方便的集成到 Hooks、CI 中自动化执行。
- 自带工具并不一定严格遵循 PEP8 等通用规范,而且不方便在协作成员中分享配置,话说回来,假如你用
PyCharm
但你的同事用VSCode
呢?
在接下来的 Hooks 和 Pipeline 两节内容中,你将进一步认识到集成第三方工具带来的优势。
Hooks
在上文中我们介绍了很多保障质量的工具,但在日常编码中,我们总不能每一次提交代码前,都把这些工具手动执行一遍吧?这看上去不是高效的做法,庆幸地是我们有现成途径解决这些问题。
Git Hooks
Git 可以在仓库中发生特定事件时自动运行自定义的脚本,这些自定义脚本即称为 hooks
。 它们让你可以在开发周期的关键点触发可定制的动作,比如每次执行提交前都执行一次 flake8
命令。大部分其他的版本控制系统也会提供类似的功能。
hooks
保存在 Git 仓库的 .git/hooks
目录中,它可以是普通的 shell 脚本,也可以是 Python、Ruby 等语言的可执行脚本,因此我们可以很方便地将常用工具集成到 hooks
中。 hooks
通常按客户端和服务端分为两大类,常用的客户端类有 pre-commit
、 prepare-commit-msg
、 commit-msg
、 post-commit
、 pre-rebase
等 hooks
,它们分别在名称所代表的事件触发时运行。
以 pre-commit
为例,该 hooks
在输入提交信息前运行。 它可以检查即将提交的快照,如果该 hooks
以非零值退出,Git 将放弃此次提交,我们可以利用它来检查提交的代码风格是否正确(运行例如 flake8
、 black
等程序)、是否引入安全风险等等。整个流程示意如下:
接下来我们将展示,如何将上文介绍的众多工具快速地集成到 pre-commit hooks
中。
pre-commit
pre-commit
是一个用于管理和维护多种语言 pre-commit hooks
的框架,就像 Python 的包管理器 pip
一样,我们可以通过 pre-commit
将他人创建并分享的 pre-commit hooks
安装到自己的项目仓库中。 pre-commit
大大减少了我们使用 git hooks
的难度,你只需要在配置文件中指定想要的 hooks
,它会替你安装任意语言编写的 hooks
并解决环境依赖问题,然后在每次提交前执行。
pre-commit
的简单使用方法如下:
通过
pip
安装pre-commit
:1
$ pip install pre-commit
pre-commit
是面向多语言的,因此它还支持通过homebrew
等方式安装。添加配置文件。
你需要创建一个名为
.pre-commit-config.yml
的文件,通常放在项目根目录下,在配置文件中我们按特定格式添加需要运行的hooks
并指定参数,示例如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
repos: - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black language_version: python3 exclude: ^migrations/|^uploads/|^scripts/|^logs/|^docs/|^dist/|^build/ - repo: https://github.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 exclude: ^migrations/|^uploads/|^scripts/|^logs/|^docs/|^dist/|^build/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.3.0 hooks: - id: check-added-large-files args: [ --maxkb=512 ]
在该示例文件中,我们添加了
black
、flake8
以及一个检查是否添加了大体积文件到 Git 的hooks
,还分别指定了语言版本、跳过检查路径等选项。配置文件的选项含义参见 https://pre-commit.com/#plugins,所有支持安装的
hooks
列表参见https://pre-commit.com/hooks.html。安装
git hook
脚本。在配置文件所在的根目录运行命令:1 2
$ pre-commit install pre-commit installed at .git/hooks/pre-commit
这会在
.git/hooks
文件夹中创建一个pre-commit
脚本文件,并在接下来你每一次提交之前运行!现在我们可以尝试提交一次代码了。(可选步骤)对所有文件运行
hooks
。通常
pre-commit
只会在触发git hooks
时对发生更改的文件运行,但我们也可以手动对当前仓库的所有文件运行:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
$ pre-commit run --all-files [INFO] Initializing environment for https://github.com/psf/black. [INFO] Initializing environment for https://github.com/pycqa/flake8. [INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks. [INFO] Installing environment for https://github.com/psf/black. [INFO] Once installed this environment will be reused. [INFO] This may take a few minutes... [INFO] Installing environment for https://github.com/pycqa/flake8. [INFO] Once installed this environment will be reused. [INFO] This may take a few minutes... [INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks. [INFO] Once installed this environment will be reused. [INFO] This may take a few minutes... black....................................................................Failed - hook id: black - files were modified by this hook reformatted /.../setup.py reformatted /.../test/generate_toc_test.py All done! ✨ 🍰 ✨ 2 files reformatted, 2 files left unchanged. flake8...................................................................Passed Check for added large files..............................................Passed
结果显示我们有文件未通过
black
检查,但black
同时也帮我们自动进行了格式化,因此只需要暂存并重新提交这些文件即可。
pre-commit hooks
的限制
使用 pre-commit
,我们可以很方便的将丰富多样的代码工具集成到 Git 的工作流中,这很大程度上提高了我们的效率,但 pre-commit hooks
本身存在以下限制:
- 客户端
hooks
并不会随代码库一起被复制,我们必须在本地仓库通过pre-commit install
执行安装之后hooks
才会生效。 - 使用
git commit --no-verify
即可绕过所有的pre-commit hooks
。
pre-commit
解决了在本地集成代码工具的问题,但在团队合作的场景下,我们希望有一种机制可以在服务器端对所有成员推送的代码进行检查,施加特定的约束,这时候就需要用到 Pipelines 了。
Pipelines
首先我们需要了解 CI/CD 这一概念。
CI/CD
CI(Continuous Integration,持续集成)是一种软件实践,它要求我们频繁地向共享仓库提交代码。更频繁地提交代码可以更快地发现错误,减少我们在发现错误时需要调试的代码量,也使得团队不同成员间的变更更容易合并。CI 可以节省我们调试错误或解决合并冲突的时间,让我们有更多的时间来编写代码。
当提交代码到共享仓库时,我们需要对每一次提交的代码进行构建和测试,以确保提交不会引入错误。这些测试既包括上文提到的 Linter,也包括安全性检查、测试覆盖率、功能测试和其他自定义检查。我们将需要一个 CI 服务器作为 Runner 以运行对提交代码的构建和检查。
CD(Continuous Deployment,持续部署)是 CI 的下一步。我们不仅在每次推送代码到共享代码库时进行构建和测试,还会在不需要任何人工干预的情况下,自动部署代码到生产环境。此外还有Continuous Delivery (持续交付),它和持续部署的区别在于需要人工干预才会执行部署。
Pipeline
Pipeline 是持续集成、交付和部署的顶层具象化组件,它由顺序执行的阶段(stage)以及每个阶段中并行的任务(job)组成。一个简单的 pipeline 如下图所示:
首先并行地执行 build
阶段的所有任务(build_a
和 build_b
),所有任务成功后继续执行下一阶段的任务,以此类推。pipeline 还能定义成按更复杂的逻辑规则运行。
下面我们以 GitLab CI 为例,展示如何配置一个 pipeline,并在测试阶段执行上文介绍的检查工具,解决不能对团队成员提交代码进行强制约束的问题。
GitLab CI 示例
配置 Runner
首先我们需要配置一个运行任务的服务器作为 Runner,这需要我们有可用的服务器主机(也可以使用本地的开发机器或购买 GitLab 提供的 Runner)。配置 Runner 整体分两步:
- 在主机上安装 GitLab Runner,GitLab Runner 是一个用于 GitLab CI/CD 并在主机上管理、执行 pipeline 任务的应用。对于不同的操作系统与架构,具体的安装步骤也有很大区别,详情参见文档。
- 为项目(或项目组)注册 Runner。这一步首先需要从 GitLab 项目主页的 Settings > CI/CD > Runners settings 获取项目的 URL 和注册令牌,然后在 Runner 所在的机器运行
register
命令使用 URL 和令牌完成注册,并选择合适的 Runner 类型,详细步骤参见文档。
配置完成后,我们将可以在 Runners settings 页面查看可用的 Runner 和状态:
创建 .gitlab.yml
我们将在项目根目录创建一个配置 pipeline 的 .gitlab.yml
文件,GitLab 将在每一次我们推送代码到共享仓库时,读取该文件中的定义和指令,创建一条不同阶段及任务组成的 pipeline,并将阶段中的任务派发给可用的 Runner 执行。我们以如下 .gitlab.yml
文件为例:
|
|
在这个文件中,我们定义了:
- Runner 应该执行的任务的结构和顺序。
- 遇到特定条件时,Runner 应做出的决定。
上面的示例定义了如下 pipeline:
before_script
关键字定义了在每个任务前执行的一组命令,我们通过这里设置的命令安装运行 pipeline 所需的依赖。stage
关键字定义了Pre-commit Hooks
和Static Analysis
两个阶段。- 之后则定义了
pre-commit
、git-lint
、mypy
和flake8
等多个任务及所属阶段。 - 每个任务中通过
script
关键字定义该任务所运行的脚本命令。 - 在
pre-commit
任务中,我们成功地将上一节介绍的pre-commit
放在服务端运行,这能确保对任何人提交的代码都会运行原先只在本地生效的pre-commit hooks
。我们还通过预定义变量和cache
关键字缓存运行任务所需的文件以加快执行速度。 - 在
mypy
任务中,我们通过allow_failure
关键字更改了任务成功才会进入下一阶段这一默认行为,并通过only
关键字限制其只对master
分支的提交执行。
关于 .gitlab.yml
的更多说明请参阅 GitLab CI/CD pipeline 配置参考手册 以及 GitLab CI/CD 示例。
查看 pipeline 的运行状态
现在当我们推送新的代码到共享仓库时,一个新的 pipeline 会被触发运行。我们可以通过以下途径在 GitLab 仓库查看 pipeline 的运行状态:
进入 CI/CD > Pipelines。将展示一个包含两个阶段的 pipeline(当前状态为已通过)。
点击 pipeline 的 ID 即可查看该 pipeline 的运行示意图:
点击一个任务名称,可查看该任务的详细运行信息:
以上即为一个完整的 GitLab CI pipeline 示例。在该 pipeline 中,我们会在每一次提交代码后:
- 对所有文件运行集成了众多
hooks
的pre-commit
,确保代码已在本地通过了pre-commit
检查。 - 单独执行
mypy
、git-lint
等检查工具。 - 还能够执行打包、运行测试、安全检查等更多更复杂的任务。
接下来我们将通过 GitHub Actions 为例,展示如何运行一个简单的 CD pipeline。
GitHub CD 示例
和 GitLab CI/CD 的区别
GitHub Actions 是 GitHub 在 2018年10月推出的持续集成服务,它和上文介绍的 GitLab CI 原理和使用方法基本相同,但存在以下区别:
- 术语定义有所不同,GitHub Actions 有以下组成部分:
- workflow (工作流程):持续集成一次运行的过程,即 GitLab 中的 pipeline。
- job (任务):一个 workflow 由一个或多个 jobs 构成,类似于 GitLab 中的 stage,但默认在不同的 Runner 并行执行,也可以设置为顺序执行。
- step(步骤):每个 job 由多个 step 构成,类似于 GitLab 中的 job,但 step 要么是一行命令,要么就是一个action,step 之间按顺序执行且能够共享数据。
- action (动作):你可以将多个脚本命令封装成一个 action,通过 GitHub 市场分享给其他人,然后在 step 中使用自建或来自社区的 action。
- GitHub 免费提供常见类型的 Runner,因此一般情况不需要再自行配置服务器及注册。
- workflow 的配置文件也是类似于
.gitlab.yml
的yaml
文件,但需要添加到项目根目录的.github/workflows
路径中。 - 可以很方便地共享或使用他人创建的脚本命令,即 action。
接下来我将演示如何使用 GitHub Actions 自动发布一个 Hugo 静态站点到 GitHub Pages。
使用 GitHub Actions 自动发布静态站点
大致的工作流程如下:
- 在本地内容仓库开发站点内容,完成后将内容推送到共享仓库。该共享仓库可能是私有的。
- 共享仓库接受推送后触发 workflow,执行以下步骤:
- 在 Runner 中拉取提交的内容。
- 配置环境并安装指定版本的 Hugo。
- 运行 Hugo 根据提交内容生成静态站点。
- 将静态文件推送到发布仓库。
- 发布仓库在接收到推送内容后会自动更新 Pages 页面。
GitHub 上有许多这类自动化部署任务的开源 Actions 项目,我们选择了其中一个简单易用的 GitHub Actions for Hugo。具体的操作步骤截图和详细配置项可以查看该项目的 README。下面简单介绍下配置过程:
在本地内容仓库中添加目录和文件:
.github/workflows/gh-pages.yml
,gh-pages.yml
文件内容如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
name: github pages on: push: branches: - main # 每次推送到 main 分支都会触发部署任务 jobs: deploy: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 with: submodules: true # Fetch Hugo themes (true OR recursive) fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: '0.79.1' extended: true - name: Build run: hugo --minify - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} external_repository: <USERNAME>/<USERNAME>.github.io # 发布仓库名称 publish_branch: main publish_dir: ./public
在该配置文件中,我们定义了如下的 workflow:
- workflow 名称为
github pages
。 - 仅在推送提交到
main
分支时执行。 - 含有一个名为
deploy
的任务,并需要在ubuntu-18.04
平台的 Runner 执行。 - 任务中的第一个步骤使用了名为
actions/checkout@v2
的action
,以拉取提交的内容到 Runner。 - 第二个步骤使用
peaceiris/actions-hugo@v2
action 配置 Hugo,并指定版本。 - 第三个步骤直接执行
hugo --minify
命令,生成静态站点(默认输出文件到public
目录) - 第四个步骤使用
peaceiris/actions-gh-pages@v3
action 将生成的静态文件推送到指定的外部仓库。
这个
workflow
基于内容仓库运行,但我们需要将运行过程生成的静态文件推送到发布仓库进行发布,因此还需要在两个仓库中分别设置密钥。- workflow 名称为
在本地生成 SSH 部署密钥:
1 2 3 4
ssh-keygen -t rsa -b 4096 -C "$(git config user.email)" -f gh-pages -N "" # 将在当前目录生成如下密钥文件: # gh-pages.pub (公钥) # gh-pages (私钥)
在 GitHub 分别进入内容仓库和发布仓库的
Settings
页面:- 将公钥
gh-pages.pub
作为Secret
添加到内容仓库,并设置Name
为ACTIONS_DEPLOY_KEY
。 - 将私钥
gh-pages
作为Deploy Key
添加到发布仓库,并设置为Allow write access
。
- 将公钥
接下来我们测试一下效果。
在本地内容仓库做一些更改,预览效果后提交并推送,然后在共享仓库的 GitHub Actions 页面检查相应 workflow
的运行状态与详细结果:
运行成功后,很快发布仓库将新增一个由该 workflow
创建的提交,相应的 GitHub Pages 也会更新相应的内容。
总结
本文中,我们围绕保障代码质量这一目的:
- 首先介绍了 Linter、Static Type Checker、Autoformatter 三种工具并各展示了一个代表性工具的使用方法。
- 将这些工具通过
pre-commit
集成到git hooks
中,在代码开发工作流中自动执行。 - 介绍了CI/CD,并以 GitLab CI pipeline 为例将以上检查部署到共享仓库,在每次代码提交后自动执行。
- 顺便介绍了一个通过 GitHub Actions 实现持续部署的示例,演示了 GitHub 持续集成服务的基本用法。
经过简单几步的一次性配置,你就可以拥有一套高度自动化的代码质量工具工作流程。但工具只是保障代码质量众多环节中最容易发力,见效快,但作用也很有限的一环。真正实现保证代码质量这一目标任重而道远,还需要我们在个人和团队这两个方面认识到代码质量的重要性,并不断践行各种代码质量设计与活动。
更新:
- 如果你觉得手动集成以上工具过于繁琐,可以尝试我制作的 Python 项目模板 Cookiecutter PyPackage,使用方式很简单,使用了 GitHub Actions 自动执行测试、预览和发布。
- 如果你想学习/提高自己的 Python 技能,我推荐一个学习网站 https://jobtensor.com/Tutorial/Python/en/Introduction,它们的内容很棒而且完全免费。