R 函数 'box::help()' 无法生成帮助文件:"Invalid Argument"

R Function 'box::help()' Cannot Generate Help File: "Invalid Argument"

动机

我和我的同事经常在 R 中创建临时脚本,以对专有数据执行 ETL 并为客户生成自动报告。为了一致性、模块化和可重用性,我正在尝试标准化我们的方法。

特别是,我想将我们最常用的函数合并到一个中央目录中,并像访问专有 R 包中的函数一样访问它们。然而,作为一名 R 开发人员,我还比较生疏,而我的队友在 R 开发方面的经验就更少了。因此,正式包的开发目前是不可行的。

方法

幸运的是,box package, by Stack Overflow's very own Konrad Rudolph, provides (among other modularity) an accessible approach to approximate the behavior of an R package. Unlike the rigorous development process outlined by the RStudio team, box requires only that one create a regular .R file, in a meaningful location, with roxygen2 documentation (#') and explicit @exports:

Writing modules

The module bio/seq, which we have used in the previous section, is implemented in the file bio/seq.r. The file seq.r is, by and large, a normal R source file, which happens to live in a directory named bio.

In fact, there are only three things worth mentioning:

  1. Documentation. Functions in the module file can be documented using ‘roxygen2’ syntax. It works the same as for packages. The ‘box’ package parses the
    documentation and makes it available via box::help. Displaying module help requires that ‘roxygen2’ is installed.

  2. Export declarations. Similar to packages, modules explicitly need to declare which names they export; they do this using the annotation comment #' @export in front of the name. Again, this works similarly to ‘roxygen2’ (but does not require having that package installed).

目前,我正在修改一个特定的模块,将其“导入”到脚本中。虽然“导入”本身可以无缝运行,但我似乎无法访问我的函数的文档。

代码

我正在 Lenovo ThinkPad 运行 Windows 10 Enterprise 上试验 box。我创建了一个名为 Script.R 的脚本,其位置作为我的工作目录。我的模块作为不起眼的文件time.R存在于相对子目录./Resources/Modules中,转载于此:

###########################
## Relative Date Windows ##
###########################

#' @title Past Day of Week
#' @description Determine the date of the given weekday that fell a given number
#'   of weeks before the given date.
#' @param from \code{Date} object. The point of reference, from which we go
#'   backwards. Defaults to current \code{Sys.Date()}.
#' @param back \code{integer}. The number of weeks to go backward from the point
#'   of reference; negative values go forward. Defaults to \code{1}, for last
#'   week. Weeks begin on \code{"Monday"}.
#' @param weekday \code{character}. The weekday within the week targeted by
#'   \code{back}; one of \code{c("Monday", "Tuesday", "Wednesday", "Thursday",
#'   "Friday", "Saturday", "Sunday")}.
#' @export
#' @return The date of the \code{weekday} falling in the week \code{back} weeks
#'   prior to the week in which \code{from} falls. Defaults to \code{"Monday"}.
past_weekday <- function(from = Sys.Date(),
                         back = 1,
                         weekday = c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
                         ) {
  cycle <- c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
  
  from <- as.Date(from)
  back <- as.integer(back)
  
  weekday_index <- (which(cycle == weekday[1]) - 1) %% 7
  
  from_index <- (which(cycle == "Sunday") + as.POSIXlt(from)$wday - 1) %% 7
  
  weekdate <- as.Date(from) - lubridate::days(from_index) - lubridate::weeks(as.numeric(back)) + lubridate::days(weekday_index)
  
  return(as.Date(weekdate))
}

遵守 roxygen2 文档,如特殊 #' 注释和 @ 标签所示。 past_weekday() 文档 比函数 本身 .

更让我感兴趣

最后也是最不重要的,这里转载Script.R本身:

# Set the working directory to the location of this very script.
setwd(this.path::this.dir())

# Access the functions in 'time.R' by relative location.
box::use(Resources/Modules/time)

# Run the function with its default values.
time$past_weekday()

# View the help page for the function.
box::help(time$past_weekday)

理论上,最后一行将通过 box::help():

显示 past_weekday() 的文档

box vignette 给出了一个简单的例子:

We can also display the interactive help for individual names using the box::help function, e.g.:

 box::help(seq$revcomp)

问题

Script.R 的前三行正是我想要的。也就是说,它们将 time 作为环境加载到 R 中,我可以从中通过 time$past_weekday() 访问 past_weekday()module$function() 语法类似于正式包中函数的限定:package::function()。事实上,past_weekday() 本身就如预期的那样工作:

time$past_weekday()
# [1] "2021-07-19"

但是,当我尝试以交互方式访问文档时

box::help(time$past_weekday)

控制台显示以下警告

Warning messages:
1: In utils::packageDescription(package, fields = "Version") :
  no package 'PKG' was found
2: In file.create(to[okay]) :
  cannot create file 'C:\Users\greg\AppData\Local\Temp\RtmpYBTTyG/.R/doc/html/module:Resources/Modules/time.html', reason 'Invalid argument'

并且交互式帮助 window 是空的,但是对于此错误消息:

对于我的团队来说,这可能是一个严重的问题。由于我们经常依赖彼此编写的有用功能,因此我们团队中的任何用户都能够轻松访问函数作者的清晰文档……就像用户习惯于使用正式的 R 包一样。如果没有这种能力,用户必须要么打扰作者进行澄清,要么在没有清楚地理解函数的目的和限制的情况下犯错误。

疑点

当我阅读警告时

In file.create(to[okay]) :
cannot create file 'C:\Users\greg\AppData\Local\Temp\RtmpYBTTyG/.R/doc/html/module:Resources/Modules/time.html', reason 'Invalid argument'

我被文件路径吸引了

C:\Users\greg\AppData\Local\Temp\RtmpYBTTyG/.R/doc/html/module:Resources/Modules/time.html

作为 Invalid argumentfile.create() 的原因。 据我所知,包含冒号 : 的目录名称 .../module:Resources/... 在 Windows and elsewhere.

上是非法的

确实,当我提供另一个非法文件路径 ./illegal:directory:name/missing.txtfile.create()

file.create('./illegal:directory:name/missing.txt')
# [1] FALSE

我收到同样的警告:

Warning message:
In file.create("./illegal:directory:name/missing.txt") :
  cannot create file './illegal:directory:name/missing.txt', reason 'Invalid argument'

罪魁祸首似乎是 this line in help.R:

display_help(doc, paste0('module:', mod_name), help_type)
#                               ^
#                             Here

但是,这个诊断似乎太简单了。坦率地说,如果在经验丰富的开发人员设计的包中发现这样的可移植性问题,我会感到非常惊讶。我发现我更有可能只是超出了我的理解范围。

我错过了什么?


更新 1

我在我的 MacBook Air 运行 Mojave 上试过了,它确实有效!虽然我仍然在控制台上收到第一条(相当奇怪的)警告消息

Warning message:
In utils::packageDescription(package, fields = "Version") :
  no package 'PKG' was found

交互式帮助 window 确实显示 预期文档:

当然,这并不能完全解决我的问题——脚本将在 VM 运行 Windows 上执行,就像我的 Lenovo 和我公司使用的所有其他计算机一样。但是,它确实支持这个问题特定于 box on Windows.

的假设

更新 2

康拉德好心confirmed that this is indeed a bug, and he's working on a fix非常感谢康拉德的澄清和回应!

如前所述,that’s a bug, now fixed

但既然我们来了,就用法说一句:

# Set the working directory to the location of this very script.
setwd(this.path::this.dir())

一般不推荐这样做。引用 Jenny Bryan:

If the first line of your R script is

setwd("C:\Users\jenny\path\that\only\I\have")

I will come into your office and SET YOUR COMPUTER ON FIRE .

‘box’也不需要这个;相反,想法是配置一个 global module search path(相当于 R 的包库,参见 .libPaths()),例如通过 box.path 选项(这通常会进入用户的 .Rprofile 配置 - 而不是 在脚本本身中!):

options(box.path = 'C:\User\Konrad\some\path')

之后,box::use将找到安装在此搜索路径中的模块。

如前所述,这应该是一个全局设置。要使用特定于项目的模块,您不会设置全局搜索路径;相反,你会使用相对导入:

box::use(./Resources/Modules/time)

无论工作目录如何,这都应该有效;它改为使用调用脚本的位置。因此,“box”永远不需要 this.path::this.dir() 或类似的 hack。为了查找数据文件,“box”提供了 box::file 功能,无论当前工作目录如何,该功能都可以使用。

另见 FAQ: how to organise globally installed modules?