Julia 学习笔记(七) | 模块开发 - 远程开发
Julia 笔记系列:
- 『Julia 初学者指南(一) | 安装、配置及编译器』
- 『Julia 初学者指南(二) | 数据类型与函数基础』
- 『Julia 学习笔记(二) | 类型,派发与设计模式』
- 『Julia 学习笔记(三) | 广播,性能和模块』
- 『Julia 学习笔记(四) | 并行计算(预备篇)』
- 『Julia 学习笔记(五) | 模块开发 - 基础篇』
- 『Julia 学习笔记(番外) | 从 Python 到 Julia』
唠唠闲话
上篇介绍了模块开发的基础知识,本篇介绍一些远程开发相关的内容,包括 GitHub 部署,测试文档覆盖率和包的注册等。概要
本篇内容包括:
模板生成
PkgTemplate
推荐阅读:PkgTemplates 官方文档
一个完整的 Julia 模块应包括:主代码,代码测试和帮助文档等内容,而这些用 PkgTemplates
就能“一键生成”。
-
调用模块
1
2# using Pkg; Pkg.add("PkgTemplates")
using PkgTemplates -
用
Template
函数设置模板参数,并存入变量t
1
2
3
4
5
6
7
8
9t = Template(;
dir=".",
plugins=[
License(; name="MIT"),
Git(; manifest=false, ssh=true),
GitHubActions(; x86=true, coverage=true),
Codecov(),
Documenter{GitHubActions}()
])参数释义:
dir
指定创建模块的位置,默认为~/.julia/dev
plugins
设置接口,也即常用的开发工具License
通过name
参数选择了 MIT 许可证Git
设置忽略Mainfest.toml
,且用ssh
链接 GitHub 仓库,默认方式是 httpsGitHubActions
启用对x86
机器的支持,并启用coverage
计算测试覆盖率Codecov()
对应上一选项的coverage=true
Documenter
指定通过 GitHubActions 部署文档
每个参数更细致的用法参考 PkgTemplates 文档的 这一节。
-
输入
t("<模块名>")
创建模块
-
查看模块文件结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15TestPackage/
├── docs
│ ├── make.jl
│ ├── Manifest.toml
│ ├── Project.toml
│ └── src
│ └── index.md
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── src
│ └── TestPackage.jl
└── test
└── runtests.jl以及隐藏文件夹
.github/
1
2
3
4
5TestPackage/.github/
└── workflows
├── CI.yml
├── CompatHelper.yml
└── TagBot.yml
只用 Template(;<参数设置>)("包名")
这一行命令,上篇提到的内容就都生成了,包括:
- Git 环境及初始 commit
- 仓库常用的
README.md
,LICENSE
和.gitignore
- 代码文档测试
src/
,docs/
和test/
- 代码环境文件
Project.toml
和Manifest.toml
- GitHub Actions 的配置文件
.github/workflows/
其他文件已介绍过就不再赘述,重点是 GitHub Actions 的配置文件:
CompatHelper.yml
:自动检查和更新依赖版本,其会根据依赖环境的版本变化,向仓库提交 PRTagBot.yml
:自动打包并发布新版本,在注册包的时候发挥作用CI.yml
:CI 全称 Continuous Integration 的缩写,即持续集成
前两个文件一般不需要修改,最后一个 CI.yml
涉及参数较多,且通常需要根据需求修改,下边着重介绍。
CI 文件
CI/CD 全称为 Continuous Integration/ Continuous Deployment,连续集成与连续部署,常见平台
文件 | 平台 |
---|---|
gitlab-ci.yml |
gitlab |
.github/workflows/xxx.yml |
github |
.travis.yml |
travis CI |
.appveyor.yml |
appveyor CI |
个人仅接触过 GitHub 平台,其他暂不讨论。下边以 QRDecoders (源文件)为例,分段理解,边用边学,且只介绍比较重要或可能需要修改的部分。
第一部分,头部内容:
1 | name: CI |
name
是当前 GitHub Action 的小标题on
的参数项push: branches:
指定会触发测试的分支,即当我们向branches
中的分支push
commit 时,将触发 CI。特别地,此处向master
分支push
会触发 CI。特别留意,PkgTemplates
生成 CI 文件的默认主分支名为main
,要根据仓库实际情况修改。
第二部分:模块测试的环境配置
1 | jobs: |
jobs: test
参数项下的 strategy
,指定了测试环境,这里用了两种方式:
- 前三个参数
version, os, arch
设置在x64
架构的ubuntu-latest
系统上,对 Julia1.6
和1.7
版本分别执行测试,共2 * 1 * 1 = 2
个测试 - 最后一个参数
include
的每条子项对应一个测试,分别指明了 Julia 版本和系统架构,version
默认取最新版本,比如目前1
等同于1.8.2
,1.6
等同于1.6.7
- 测试系统设置越多,每次触发 CI 需要等待的时间可能就越长,所以通常还要根据实际情况调整
此外, name
字段指定了 GitHub Actions 子项的名称由,如下图
第三部分,测试内容:
1 | #jobs: |
jobs: test: steps
部分参数释义:
julia-actions/setup-julia@v1
启动 Juliajulia-actions/julia-buildpkg@v1
编译 Julia 依赖环境julia-actions/julia-runtest@v1
执行测试,也即仓库下的test/runtests.jl
文件julia-actions/julia-processcoverage@v1
计算覆盖率,主要为Codecov
服务提供数据codecov/codecov-action@v2
触发Codecov
服务,生成覆盖率报告
对应到仓库中,显示信息如下
第四部分,文档参数:
1 | #jobs: |
jobs: docs:
部分参数释义:
name
指定 GitHub Actions 子项的名称runs-on: ubuntu-latest
指定在ubuntu-latest
系统上生成文档julia-actions/julia-docdeploy@v1
这部分应该是在执行run
参数的内容,实际执行内容与docs/make.jl
有关- 特别留意一项
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
,由于文档部署涉及对仓库的读写,这里要设置GITHUB_TOKEN
,允许Documenter
对gh-pages
分支的读写 - 其他参数与上一部分类似
对应到仓库中,显示信息如下
总之,留意 push
的默认分支,根据需求修改测试数目,其他基本不需要改动。
关于 Documenter
的使用,以及 GITHUB_TOKEN
的设置,我们接下来进行介绍。
DocumentTools
推荐阅读:Documenter 文档 以及 DocumenterTools 文档
-
激活当前环境,输入模块名称,生成密钥
1
2
3using <包名称>
using DocumenterTools
DocumenterTools.genkeys(<包名称>)注意不是输入字符串,而是将当前环境的
模块名
作为参数 -
根据提示内容复制第一部分密钥,建议先用 git 连接远端的
GitHub
仓库,这一来可以点击Info
的链接直接跳转
-
进入仓库,点击设置,找到
Deploy keys
粘贴刚刚复制的内容
注意 ssh 密钥会赋予
Documenter
写入权限,所以注意是设置仓库密钥,不要设置成个人账号的密钥 -
其他参数参见 DocumenterTools 文档,比如修改
user
参数1
DocumenterTools.genkeys(; user="用户名", repo="仓库名")
docs/make.jl
和文档 docs/index.md
的内容,下一章节一块介绍。
测试,文档和覆盖率
代码测试
-
Julia 规定测试代码放在
test/
目录下的文件runtests.jl
中,在包模式下执行test
会自动运行test/runtest.jl
文件1
2
3; # 进入 shell 模式
mkdir test
touch test/runtests.jl -
设置测试环境,也可以在
Project.toml
的extras
中添加相应依赖1
2
3] # 进入包管理模式
activate test # 切换环境到 test 目录
add Test # 添加测试库 Test如果在包环境下执行
test
报错cannot merge projects
,可能是测试环境的Manifest.toml
与主环境重复了,删除即可,参考 GitHub 的讨论
帮助文档
文档通常放在 docs/
目录下,Julia 用得最广的文档工具为 Documenter.jl
,其他工具暂不介绍。
-
Documenter.jl
约定用docs/make.jl
作为文档的主文件1
2
3; # 进入 shell 模式
mkdir docs
touch docs/make.jl -
类似地,为文档设置开发环境
1
2
3] # 进入包管理模式
activate docs # 切换环境到 docs 目录
add Documenter如果包未注册,可能需要添加
dev .
将开发中的包加入到doc
环境依赖中,并添加Manifest.toml
到 Git 记录 -
以
QRDecoders
为例,在docs/make.jl
中添加如下内容1
2
3
4
5
6
7
8
9
10DocMeta.setdocmeta!(QRDecoders, :DocTestSetup, :(using QRDecoders); recursive=true)
makedocs(;
modules=[QRDecoders],
sitename="QRDecoders.jl"
)
deploydocs(;
repo="github.com/JuliaImages/QRDecoders.jl",
)其中
deploydocs
设置文档部署的仓库地址 -
文档默认主页面根据
docs/src
目录下的index.md
进行渲染。文件内容支持许多规则,比如用@docs
和函数名可以将函数的帮助文档加入到页面中,如下图
对应到网页上,每个函数每个派发的文档都会展示出来
-
除了通过 GitHub 部署网页,也可以在本地生成预览,命令如下
1
julia --project=docs/ docs/make.jl
激活
docs
环境并执行make.jl
文件,执行后将在docs/build
目录下生成网页,执行下边命令并打开浏览器1
python3 -m http.server --directory docs/build/ 8181
其中
8181
为自定义的端口号,在网页中输入http://localhost:8181
即可预览
网页是基于 Markdown 语法编写的,相关规则以及 Julia 中的特殊用法推荐看社区的帖子:Markdown.jl 使用总结或者 Julia 官方的 Markdown 手册。
代码覆盖率
CodeCov 是非常实用的开发工具,很多开源仓库都用它来查看主代码文件被测试覆盖的比例,其对应由 CI.yml
文件中的 julia-actions/julia-processcoverage@v1
触发 。如果覆盖率降低会发出提示:
提示显示了主文件覆盖率的变化
点击查看代码,标红说明该行代码未被测试覆盖,也即存在错误的可能但未被测试捕捉
上图说明 add!
函数的测试样例不够全面,没有处理 val = 0
的情况。如果这里 deleteat!
错写成 delete!
,错误也不会被发现。尽管bug 可能在之后被发现,但用 CodeCov
能提前规避潜在的错误。
一般地,代码覆盖率更高,说明作者代码认真做了测试,仓库可靠性也更高。
除此之外还有性能测试 bench.jl
等等很多可选内容,后续接触再进一步学习。
包注册
推荐阅读
Julia 包注册说明:General registry README
自动合并说明: AutoMerge guidelines
注册机器人:Registrator
模块命名规范:Package naming guidelines
这部分强烈建议看官方文档,包括常见 FAQ,以及自动合并的规则,帖子只简单介绍注册流程,遇到问题再查阅官方说明就行了。
JuliaRegistries/General 是 Julia 包的注册表,其维护关于 Julia 包的信息,例如版本、依赖项和兼容性限制。
一般地,我们通过 JuliaRegistrator 向仓库提交 PR,更新包相关的 .toml
文件。相关文件并入主分支后,用户就能通过 Pkg.add
安装这个包了。
目前等待期如下:
- 新的 Julia 包:3 天(这让社区有时间反馈)
- 现有软件包的新版本:15 分钟
- JLL 包(二进制依赖项):15 分钟,对于新包或新版本
总之,包在第一次注册如果满足自动合并的要求,等待三天后就会自动合并仅仓库,此后升级小版本只需要等待 15 分钟。
注册机器人
进入包所在 GitHub 仓库,通过 @JuliaRegistrator
提出注册请求。
-
方法一:在 issue 中输入
@JuliaRegistrator register
,机器人就会根据Project.toml
内容,自动生成 PR,并回复注册信息
-
方法二:打开最近的一个 commit,选择一行或者在底部输入消息:
-
方法三:在 JuliaHub 中点击注册
这种方式注册的包可以不局限 GitHub 仓库
如果触发机器人后,因为测试问题没有通过,则在个人仓库修改密码并重新输入 @JuliaRegistrator register
触发机器人。
自动合并
官方列举了自动合并的要求,这里挑几个例子:
- 包名称应以大写字母开头,仅包含 ASCII 字母数字字符,并且至少包含一个小写字母。
- 名称长度至少为 5 个字符。
- 名称不包括“julia”或以“Ju”开头。
- 有一个上限
[compat]
条目 julia 版本,它只包括有限数量的 Julia 重大版本。 - 依赖项:所有依赖项都应具有
[compat]
上限条目 - 许可证:包应该有一个 OSI 批准的软件许可证,位于包代码的顶级目录中,例如在一个名为LICENSE或的文件中LICENSE.md
- 为防止名称相似的包之间的混淆,新包的名称还必须满足以下三项检查:
- 包名称与任何现有包的名称之间的最小编辑距离必须至少为 3
- 包名称的小写版本与任何现有包名称的小写版本之间的最小编辑距离必须至少为 2。
- 包名称和任何现有包之间的VisualStringDistances.jl 的视觉距离必须超过某个手动选择的阈值(当前为 2.5)
值得留意的几点,包需要符合命名规范;确保不会有相近命名的包;确保包的依赖项都有 [compat]
条目(自带库比如 SparseArray
等不需要指定)。
如果自动合并失败,可以在 PR 中说明原因,然后等待人工审核。
TagBot
包注册成功后,第一步用 PkgTemplates.jl 生成的 TagBot.yml
会让仓库自动生成 Release。此时,仓库的 tag
标签会记录该版本的状态
GitHub 右侧也可以看到 Release 的状态
小结
一般场景中,先用 PkgTemplates.jl
生成代码文件和部署文件,然后 DocumenterTools
生成密钥并添加到远端,剩下就是常规的代码编写了,非常简单。
这些是最基础的用法,比如 PkgTemplates.jl
还能制作模板,文档测试还有其他工具等等,未来可以再一边摸索。
附录-函数文档
这部分参考了 Document 官网教程,可以看成 Documenter 前几节的翻译,更建议直接看官方文档。
-
Julia 允许包开发人员能轻松地为函数、类型和其他对象编写文档。基本语法很简单:出现对象(函数、宏、类型或实例)顶部编写的字符串将被解释为文档(docstrings)。请注意,文档字符串和文档对象之间不能有空行或注释。这是一个基本示例:
1
2"Tell whether there are too foo items in the array."
foo(xs::Array) = ... -
这是一个更复杂的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14"""
bar(x[, y])
Compute the Bar index between `x` and `y`.
If `y` is unspecified, compute the Bar index between all pairs of columns of `x`.
# Examples
```julia-repl
julia> bar([1, 2], [1, 2])
1
```(排版原因忽略这个括号及内容)
"""
function bar(x, y=1) end显示效果如下:
-
补充说明:
- 顶部前边放四个空格,可将函数名高亮,当第二参数可选时,应写为
bar(x[, y])
- 在函数签名后,用单行句描述函数用途,如果需要的话,在一个空行之后,在第二段提供更详细的信息。撰写函数的文档时,单行语句应使用祈使结构(比如「Do this」、「Return that」)而非第三人称(不要写「Returns the length…」),并且应以句号结尾。如果函数的意义不能简单地总结,更好的方法是分成分开的组合句
- 不要自我重复:因为签名给出了函数名,所以没有必要用「The function bar…」开始文档:直接说要点。类似地,如果签名指定了参数的类型,在描述中提到这些是多余的。
- 顶部前边放四个空格,可将函数名高亮,当第二参数可选时,应写为
-
在
# Arguments
下插入参数列表:只在确实必要时提供参数列表,对于简单函数,直接在函数目的中描述更合适。但对于拥有多个参数的(特别是含有关键字参数的)复杂函数来说,提供一个参数列表是个好主意,比如1
2
3# Arguments
- `n::Integer`: the number of elements to compute.
- `dim::Integer=1`: the dimensions along which to perform the computation. -
用
See also:
提供关联函数的引用,比如1
See also [`bar!`](@ref), [`baz`](@ref), [`baaz`](@ref).
-
在
# Examples
中包含一些代码例子,示例应尽可能按doctest
来写,具体在 Documenter 中一并介绍,示例:1
2
3
4
5
6
7
8
9
10
11"""
Some nice documentation here.
# Examples
```jldoctest
julia> a = [1 2; 3 4]
2×2 Array{Int64,2}:
1 2
3 4
```(排版原因忽略这个括号及内容)
""" -
使用反引号表示代码和方程,LaTeX 方程使用两个反引号,比如
1
2`a = 1`
``α = 1`` -
结尾的
"""
应单独一行(字符串末尾的空白字符会被自动strip
掉) -
代码中应遵守单行长度限制,即建议
92
个字符后换行 -
对长文档字符串,可以考虑使用
# Extend help
来拆分文档 -
函数存在多个方法时,每个方法会用函数
catdoc
拼接
以上。