R语言 如何有条件地为来自另一个包的S3泛型提供S3方法?

dwbf0jvd  于 2023-06-27  发布在  其他
关注(0)|答案(2)|浏览(142)

我正在做一个数据操作包,它在引擎盖下使用了一些其他的库。假设我的数据总是有一个类"custom",我有一个函数custom_select()来选择一些列。
我希望我的包有很少的依赖,但也有类似的语法从dplyr的函数。因为有几个dplyr函数是泛型,所以我可以对不同的输入类型使用相同的函数名。在我的情况下,我可以创建一个方法select.custom(),这样用户就可以将data.framecustom对象传递给select(),两者都可以工作。
现在,根据我的理解,这需要将dplyr放在Imports中,因为我需要访问它的select()泛型。我想避免这样做,因为我想限制硬依赖的数量。
我想到的场景是:

  • 用户已经加载了dplyr,那么他们可以使用select()和类custom的数据,它应该工作
  • 用户没有安装/加载dplyr,我不想强迫他们安装/加载,所以他们可以使用custom_select()函数。

理想情况下,我想把dplyr放在Suggests中,这样就不是绝对必要的,但如果用户有它,它会增加一些东西。

示例

custom.R

#' @export
#' @importFrom dplyr select
custom_select <- function(data, select) {
  print("Hello, world!")
}

#' @export
select.custom <- custom_select

NAMESPACE

# Generated by roxygen2: do not edit by hand

export(custom_select)
export(select.custom)
importFrom(dplyr,select)

R CMD检查错误,如果我没有把dplyr放在Imports中,把它放在Suggests中也不起作用(两种情况下都是相同的错误):

❯ checking package dependencies ... ERROR
  Namespace dependency missing from DESCRIPTION Imports/Depends entries: 'dplyr'

综上所述,如果dplyr的泛型可用的话,有没有一种方法可以让dplyr摆脱硬依赖,同时仍然为dplyr的泛型提供方法?
**编辑:**我尝试了@VonC的答案,但无法使其工作。在下面的示例中,dplyr在我的自定义包之前加载,因此select.custom()应该可用,但实际上不是:

library(dplyr, warn.conflicts = FALSE)
library(custompackage)

foo <- letters
class(foo) <- "custom"

custom_select(foo)
#> [1] "Hello, world!"
select(foo)
#> Error in UseMethod("select"): no applicable method for 'select' applied to an object of class "custom"

以下是重要文件:
custom.R

#' @export
custom_select <- function(data, select) {
  print("Hello, world!")
}

if (requireNamespace("dplyr", quietly = TRUE)) {
  select.custom <- function(data, select) {
    custom_select(data, select)
  }
  utils::globalVariables("select.custom")
}

NAMESPACE

# Generated by roxygen2: do not edit by hand

export(custom_select)

DESCRIPTION(无Imports

[...]
Suggests:
  dplyr
h9a6wy2h

h9a6wy2h1#

你需要把dplyr放在Enhances中,然后使用.onLoaddplyr命名空间中有条件地注册你的方法,具体取决于dplyr是否在 load time 时安装。

nm <- package <- "TestPackage"
dir.create(file.path(package,     "R"), recursive = TRUE)
dir.create(file.path(package,   "man"), recursive = TRUE)
dir.create(file.path(package, "tests"), recursive = TRUE)

cat(file = file.path(package, "DESCRIPTION"), "
Package: TestPackage
Version: 0.0-0
License: GPL (>= 2)
Description: A (one paragraph) description of what
  the package does and why it may be useful.
Title: My First Collection of Functions
Author: First Last [aut, cre]
Maintainer: First Last <First.Last@some.domain.net>
Enhances: dplyr
")

cat(file = file.path(package, "NAMESPACE"), "
export(selectDotZzz)
")

cat(file = file.path(package, "R", paste0(nm, ".R")), "
selectDotZzz <- function(.data, ...) 0
.onLoad <- function(libname, pkgname) {
    if(requireNamespace(\"dplyr\", quietly = TRUE))
        registerS3method(\"select\", \"zzz\", selectDotZzz,
                         envir = asNamespace(\"dplyr\"))
}
")

cat(file = file.path(package, "man", paste0(nm, ".Rd")), "
\\name{whatever}
\\alias{selectDotZzz}
\\title{whatever}
\\description{whatever}
")

cat(file = file.path(package, "tests", paste0(nm, ".R")),
    sprintf("library(%s)", nm))
cat(file = file.path(package, "tests", paste0(nm, ".R")), append = TRUE, "
if(requireNamespace(\"dplyr\", quietly = TRUE))
    stopifnot(identical(dplyr::select(structure(0, class = \"zzz\")), 0))
")

getRversion()
packageVersion("dplyr")
tools:::Rcmd(c("build", package))
tools:::Rcmd(c("check", Sys.glob(paste0(nm, "_*.tar.gz"))))

unlink(Sys.glob(paste0(nm, "*")), recursive = TRUE)

相关输出:

> getRversion()
[1] '4.3.1'
> packageVersion("dplyr")
[1] '1.1.2'
> tools:::Rcmd(c("build", package))
* checking for file 'TestPackage/DESCRIPTION' ... OK
* preparing 'TestPackage':
* checking DESCRIPTION meta-information ... OK
* checking for LF line-endings in source and make files and shell scripts
* checking for empty or unneeded directories
* building 'TestPackage_0.0-0.tar.gz'
> tools:::Rcmd(c("check", Sys.glob(paste0(nm, "_*.tar.gz"))))
* using log directory '/Users/mikael/Desktop/R-experiments/codetools/TestPackage.Rcheck'
* using R version 4.3.1 Patched (2023-06-19 r84580)
* using platform: aarch64-apple-darwin22.5.0 (64-bit)
* R was compiled by
    Apple clang version 14.0.3 (clang-1403.0.22.14.1)
    GNU Fortran (GCC) 12.2.0
* running under: macOS Ventura 13.4
* using session charset: UTF-8
* checking for file 'TestPackage/DESCRIPTION' ... OK
* this is package 'TestPackage' version '0.0-0'
* checking package namespace information ... OK
* checking package dependencies ... OK
* checking if this is a source package ... OK
* checking if there is a namespace ... OK
* checking for executable files ... OK
* checking for hidden files and directories ... OK
* checking for portable file names ... OK
* checking for sufficient/correct file permissions ... OK
* checking whether package 'TestPackage' can be installed ... OK
* checking installed package size ... OK
* checking package directory ... OK
* checking DESCRIPTION meta-information ... OK
* checking top-level files ... OK
* checking for left-over files ... OK
* checking index information ... OK
* checking package subdirectories ... OK
* checking R files for non-ASCII characters ... OK
* checking R files for syntax errors ... OK
* checking whether the package can be loaded ... OK
* checking whether the package can be loaded with stated dependencies ... OK
* checking whether the package can be unloaded cleanly ... OK
* checking whether the namespace can be loaded with stated dependencies ... OK
* checking whether the namespace can be unloaded cleanly ... OK
* checking loading without being on the library search path ... OK
* checking startup messages can be suppressed ... OK
* checking dependencies in R code ... OK
* checking S3 generic/method consistency ... OK
* checking replacement functions ... OK
* checking foreign function calls ... OK
* checking R code for possible problems ... OK
* checking Rd files ... OK
* checking Rd metadata ... OK
* checking Rd cross-references ... OK
* checking for missing documentation entries ... OK
* checking for code/documentation mismatches ... OK
* checking Rd \usage sections ... OK
* checking Rd contents ... OK
* checking for unstated dependencies in examples ... OK
* checking examples ... NONE
* checking for unstated dependencies in 'tests' ... OK
* checking tests ...
  Running ‘TestPackage.R’
 OK
* checking PDF version of manual ... OK
* DONE

Status: OK
pbgvytdp

pbgvytdp2#

你可以通过在if语句中定义方法来检查包是否可用,并使用utils::globalVariables()来避免R CMD检查未定义的全局函数或变量。
通常不允许改变另一个包的命名空间according to CRAN policies,并且尝试直接将函数分配给dplyr::select.custom可能是不可接受的。
因此,我们的想法是在全局环境中将该方法分配给select.custom,这将允许它在加载dplyr时用作通用select()函数的方法。关键点是您没有直接修改dplyr名称空间。
您需要调整custom.R文件:

#' Custom select function
#'
#' @export
custom_select <- function(data, select) {
  print("Hello, world!")
}

if (requireNamespace("dplyr", quietly = TRUE)) {
  select.custom <- function(data, select) {
    custom_select(data, select)
  }
  utils::globalVariables("select.custom")
}

对于NAMESPACE文件,您可以避免从dplyr导入任何内容:

# Generated by roxygen2: do not edit by hand

export(custom_select)

DESCRIPTION文件中,您应该在Suggests下列出dplyr

Suggests:
    dplyr

这样,如果用户加载了dplyr,则select.custom方法将可用。如果未加载dplyr,用户仍然可以使用custom_select()功能。这种方法将dplyr排除在硬依赖项之外,同时仍然为dplyr的泛型提供方法(如果可用的话)。
这将在包的命名空间中创建一个函数select.custom,当dplyr包加载时,该函数将用作select()的方法。请注意,这不会修改dplyr名称空间本身。
另外,最好说明select.custom功能只有在安装并加载了dplyr时才可用,并且dplyr是建议的依赖项,而不是必需的。
DESCRIPTION文件中,您应该在Suggests下列出dplyr

Suggests:
    dplyr

这种方法应该符合CRAN策略,因为您不会更改另一个包的名称空间,并且是基于dplyr包的可用性有条件地定义方法。
话虽如此,Mikael Jagan在“编写R扩展/创建R包/包结构/包依赖/建议的包”的评论中指出

警告:如果你做的事情会在安装时运行,这取决于建议的软件包是否可用,这包括R代码文件中的顶级代码,.onLoad函数以及S4类和方法的定义,请非常小心。

问题是,一旦加载了建议的包的命名空间,对它的引用可能会在安装的包中捕获(最常见的是在S4方法中),但是当使用安装的包时,建议的包可能不可用(特别是对于二进制包可能在不同的机器上)。
更糟糕的是,问题可能不仅限于您的包,因为您建议的包的名称空间也会在任何导入您的包的包安装时加载,因此可能会在那里被捕获。

相关问题