0%

R package development

本文简单记录R包开发的流程

准备

创建R包需要用到的包 devtools,以及usethis

1
2
3
install.packages(c("devtools","usethis"))
library(devtools)
library(usethis)

创建Git repo (可选)

如果需要使用Git管理R包,我们先要在GitHub上创建该包的Git repo,

在GitHub登陆后,进入[Your Repositories] → [New]

创建一个叫”regexcite”的库进行练习

然后,clone这个仓库到本地,再开始搭建我们的包

1
git clone https://github.com/thereallda/regexcite.git

注意,这里要将https://github.com/thereallda/regexcite.git换成你创建的地址。

初始化R包

打开RStudio,使用函数 usethis::create_package() 初始化克隆到本地电脑的包的目录 (最好是绝对路径)

1
usethis::create_package("~/path/to/regexcite")

使用后在目录(”~/path/to/regexcite”)下创建以下文件和目录:

1
2
3
4
5
6
7
8
.Rbuildignore*
.Rhistory*
.Rproj.user/
.gitignore*
DESCRIPTION*
NAMESPACE*
R/
regexcite.Rproj*
  • .Rbuildignore lists files that we need to have around but that should not be included when building the R package from source.

  • .Rproj.user, if you have it, is a directory used internally by RStudio.

  • .gitignore anticipates Git usage and ignores some standard, behind-the-scenes files created by R and RStudio. Even if you do not plan to use Git, this is harmless.

  • DESCRIPTION provides metadata about your package. We edit this shortly.

  • NAMESPACE declares the functions your package exports for external use and the external functions your package imports from other packages. At this point, it is empty, except for a comment declaring that this is a file we will not edit by hand.

  • The R/ directory is the “business end” of your package. It will soon contain .R files with function definitions.

  • regexcite.Rproj is the file that makes this directory an RStudio Project. Even if you don’t use RStudio, this file is harmless. Or you can suppress its creation with create_package(…, rstudio = FALSE).

创建第一个函数

函数的脚本应当存放在 R/ 目录下。可以直接创建脚本保存于其中。也可以使用函数 use_r() 创建

1
use_r("strsplit1")

use_r("strsplit1") 直接创建”strsplit1.R”到 R/

我们写下第一个函数 strsplit1 是对 base::strsplit() 的包装,原函数返回一个 list ,而 strsplit1 只取返回结果的第一个元素,相当于是 unlist(strsplit(x, split)) .

1
2
3
4
5
# split a single string
strsplit1 <- function(x, split) {
strsplit(x, split = split)[[1]]
}

测试第一个函数

load_all() 读入我们在 R/ 目录下所有的脚本。这样就可以载入刚写的 R/strsplit1.R

1
2
3
load_all()
(x <- "alfa,bravo,charlie,delta")
strsplit1(x, split = ",")

实际上,load_all() 并不会把函数载入到我们的全局环境(global environment)中,而是用一种 library() 的方式载入,可以使用以下命令检查:

1
exists("strsplit1", where = globalenv(), inherits = FALSE)

这里应当返回 FALSE , 如果返回 TRUE 可以通过重启R清空全局环境,再运行一次 load_all() 即可。

load_all() simulates the process of building, installing, and attaching the regexcite package.

检查R包构建情况

使用 check() 函数可以检查R包构建情况,同时也会自动更新文档之类的。

1
2
3
4
5
6
7
8
9
10
11
check()
-- R CMD check results ------------------------------------------------------------------ regexcite 0.0.0.9000 ----
Duration: 7.5s

> checking DESCRIPTION meta-information ... WARNING
Non-standard license specification:
`use_mit_license()`, `use_gpl3_license()` or friends to pick a
license
Standardizable: FALSE

0 errors √ | 1 warning x | 0 note √

这里提示我们没有选 license

https://blog.csdn.net/midnight_time/article/details/83989131

MIT: 软件可以随便用,随便改

GPL3 / Apache2: 软件可以随便用,但不能随便改

我们使用 MIT license usethis::use_mit_license()

1
2
3
4
5
> use_mit_license()
√ Setting License field in DESCRIPTION to 'MIT + file LICENSE'
√ Writing 'LICENSE'
√ Writing 'LICENSE.md'
√ Adding '^LICENSE\\.md$' to '.Rbuildignore'

LICENSE 文件内应该是:

1
2
YEAR: 2022
COPYRIGHT HOLDER: regexcite authors

修改 DESCRIPTION

打开 DESCRIPTION 文件进行修改,里面有一些样式的内容,主要改一下 Authors@RDescription 区域

改好之后是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Package: regexcite
Title: Toy package for paracticing the process about developing R package.
Version: 0.0.0.9000
Authors@R:
person(given = "Dean",
family = "Li",
role = c("aut", "cre"),
email = "")
Description: Convenience functions to make some common tasks with string
manipulation and regular expressions a bit easier.
License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
license
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.1.1

添加说明文档

通过 roxygen2 注释系统在脚本内部写下函数有关的文档。

在RStudio内,移动光标到函数的代码区段,点击 Code > Insert Roxygen Skeleton 将会自动添加 #' 开头的 roxygen2 注释

修改这段注释,在相应区域内写上对应的描述即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#' Split a string
#'
#' @param x A character vector with one element.
#' @param split What to split on.
#'
#' @return A character vector.
#' @export
#'
#' @examples
#' x <- "alfa,bravo,charlie,delta"
#' strsplit1(x, split = ",")
strsplit1 <- function(x, split) {
strsplit(x, split = split)[[1]]
}

运行 document() 命令自动生成文档

1
2
3
4
5
> document()
i<U+00A0>Updating regexcite documentation
i<U+00A0>Loading regexcite
Writing NAMESPACE
Writing NAMESPACE

?strsplit1 可以查看该文档

同时, NAMESPACE 文件也会被写入以下内容

1
2
3
# Generated by roxygen2: do not edit by hand

export(strsplit1)

最后,再 check() 一遍

1
2
3
4
5
> check()
-- R CMD check results ------------------------------------------------------------------ regexcite 0.0.0.9000 ----
Duration: 9s

0 errors √ | 0 warnings √ | 0 notes √

安装并载入

上述结果表明包构建没问题,可以通过 install() 安装当前目录的这个包

1
install()

载入并测试

1
2
3
4
library(regexcite)

x <- "alfa,bravo,charlie,delta"
strsplit1(x, split = ",")

输出结果与测试一致,说明这个测试包构建成功了!

测试包

use_testthat()声明我们要对该包进行测试,会创建tests目录,及其他用于自动测试的相关文件

1
2
3
4
5
6
7
> use_testthat()
√ Setting active project to 'D:/R_practice/regexcite'
√ Adding 'testthat' to Suggests field in DESCRIPTION
√ Setting Config/testthat/edition field in DESCRIPTION to '3'
√ Creating 'tests/testthat/'
√ Writing 'tests/testthat.R'
* Call `use_test()` to initialize a basic test file and open it for editing.

添加对函数的测试脚本

1
2
3
> use_test("strsplit1")
√ Writing 'tests/testthat/test-strsplit1.R'
* Modify 'tests/testthat/test-strsplit1.R'

这会创建一个测试脚本 test-strsplit1.R ,我们需要往里面写入对函数的测试方法,以及期望输出

1
2
3
4
test_that("strsplit1() splits a string", {
expect_equal(strsplit1("a,b,c", split = ","), c("a", "b", "c"))
})

使用 test() 进行测试

1
2
3
4
5
6
7
8
9
10
> test()
i<U+00A0>Loading regexcite
i<U+00A0>Testing regexcite
√ | OK F W S | Context
√ | 1 | strsplit1 [0.2 s]

== Results ================================================================================================================
Duration: 0.2 s

[ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ]

注意使用testthat进行测试不是必须的,只要你确保函数都可以运行,就可以跳过这一步。

引用外部R包

如果需要使用别的包的函数,我们需要对其进行引用。

通过 use_package() 可以在 DESCRIPTION 文件中加入引用外部包的说明(手动写入应当也可以)。

这里我们引用 stringr

1
2
3
use_package("stringr")
√ Adding 'stringr' to Imports field in DESCRIPTION
* Refer to functions with `stringr::fun()`

使用后在 DESCRIPTION 文件插入一段Imports

1
2
Imports: 
stringr

另外,在函数的 roxygen2区域需要加入引用函数的说明,例如使用stringr::str_split需要写入以下内容:

1
#' @importFrom stringr str_split

创建一个 stringr 版本的函数

1
2
3
4
5
6
7
8
str_split_one <- function(string, pattern, n = Inf) {
stopifnot(is.character(string), length(string) <= 1)
if (length(string) == 1) {
stringr::str_split(string = string, pattern = pattern, n = n)[[1]]
} else {
character()
}
}

保存后,使用 rename_files() 修改

1
2
√ Moving 'R/strsplit1.R' to 'R/str_split_one.R'
√ Moving 'tests/testthat/test-strsplit1.R' to 'tests/testthat/test-str_split_one.R'

更改 test-str_split_one.R 的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
test_that("str_split_one() splits a string", {
expect_equal(str_split_one("a,b,c", ","), c("a", "b", "c"))
})

test_that("str_split_one() errors if input length > 1", {
expect_error(str_split_one(c("a,b","c,d"), ","))
})

test_that("str_split_one() exposes features of stringr::str_split()", {
expect_equal(str_split_one("a,b,c", ",", n = 2), c("a", "b,c"))
expect_equal(str_split_one("a.b", stringr::fixed(".")), c("a", "b"))
})

使用 document() 重新生成文档

1
2
3
4
5
6
7
8
9
10
> document()
i<U+00A0>Updating regexcite documentation
i<U+00A0>Loading regexcite
Writing NAMESPACE
Writing NAMESPACE
Writing str_split_one.Rd
Deleting strsplit1.Rd
Warning message:
In setup_ns_exports(path, export_all, export_imports) :
Objects listed as exports, but not present in namespace: strsplit1

现在测试新写的 str_split_one()

1
2
load_all()
str_split_one("a, b, c", pattern = ", ")

测试完成后,add, commit and push

1
2
3
$ git add .
$ git commit -m "change to str_split_one"
$ git push

最后再 check() 一次,没有问题的话就可以安装使用了

build ignore

如果想要在构建R包的时候忽略某些文件或文件夹,可以使用 usethis::use_build_ignore()

1
use_build_ignore(c('data/', 'data-raw/'))

向GitHub提交修改

测试无误后,可以将我们创建好的R包commit到GitHub

1
2
3
git add ./
git commit -m "Initial commit"
git push

总结

R包的开发流程概况下来可以分为以下步骤:

  1. 如果需要在GitHub上管理,创建GitHub repo for R package,并Clone GitHub repo到本地(optional)
  2. usethis::create_package() 将目录初始化为R包的结构
  3. 修改 DESCRIPTION ,并选择license usethis::use_mit_license() or usethis::use_gpl3_license()
  4. R\ 目录下添加脚本并写入函数 use_r() ;如果需要引用外部R包,则用 use_package() 引入
  5. 测试函数
  6. roxygen2 格式写文档,document()
  7. 检查R包构建情况,没问题就安装测试 check() and install()
  8. README文档帮助他人了解你的包 use_readme_rmd() and build_readme()
  9. 同步到GitHub, git add, commit and push (optional)

Ref:

https://r-pkgs.org/whole-game.html