rr-packager-s3

How do I conditionally provide S3 methods for S3 generics from another package?


I'm making a package for data manipulation that uses some other libraries under the hood. Let's say that my data always has a class "custom" and that I have a function custom_select() to select some columns.

I would like my package to have few dependencies but also a similar syntax as functions from dplyr. Because several dplyr functions are generics, I can use the same function names for a different input type. In my situation, I could make a method select.custom() so that the user can either pass a data.frame or a custom object to select() and both would work.

Now from my understanding, this requires putting dplyr in Imports because I need to have access to its select() generic. I'd like to avoid doing this because I want to limit the number of hard dependencies.

The scenario I have in mind is:

Ideally, I'd like to put dplyr in Suggests so that it's not strictly necessary but it adds something if the user has it.


Example

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 check errors if I don't put dplyr in Imports and putting it in Suggests also doesn't work (same error for both cases):

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

In summary, is there a way to keep dplyr out of hard dependencies while still providing methods for dplyr's generics if it is available?



Edit: I tried @VonC's answer but couldn't make it work. In the example below, dplyr is loaded before my custom package so select.custom() should be available but isn't:

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"

Here are the important files:

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 (no Imports)

[...]
Suggests:
  dplyr

Solution

  • You need to put dplyr in Enhances and use .onLoad to conditionally register your method in the dplyr namespace, depending on whether dplyr is installed at 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)
    

    The relevant output:

    > 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