rr-packager-s3

What is the correct way of extending an S3 class from another package in R


I am writing an R package pkgA that is extending another package, pkgB. In particular, I implemented a new class gas_planet that extends the functionality of a class planet written in pkgB. When I use devtools::load_all(), I have no problems in extending a generic function pkgB::hello() to hello.gas_planet(). However, when running devtools::check(), examples calling the generic function from pkgB halt with the following error:

> checking examples ... ERROR
  Running examples in 'pkgA-Ex.R' failed
  The error most likely occurred in:
  
  > base::assign(".ptime", proc.time(), pos = "CheckExEnv")
  > ### Name: hello.gas_planet
  > ### Title: Say hello to a gas planet
  > ### Aliases: hello.gas_planet
  > 
  > ### ** Examples
  > 
  > planet <- set_gas_planet("Jupiter")
  > hello(planet)
Error in hello(planet) : could not find function "hello"
Execution halted

The code from the mother package pkgB is:

#' Set planet
#'
#' @param name planet name
#'
#' @return planet object
#' @export
set_planet <- function(name){
  planet <- list(name = name)
  class(planet) <- "planet"
  return(planet)
}


#' Say hello to a planet
#'
#' @param planet a planet object
#' @param ... More arguments
#'
#' @export
hello <- function(planet, ...) {
  UseMethod("hello", planet)
}

with NAMESPACE

# Generated by roxygen2: do not edit by hand

export(hello)
export(set_planet)

My child package pkgA contains the code:

#' Set gas planet
#'
#' @inheritParams pkgB::set_planet
#'
#' @return planet object
#' @export
set_gas_planet <- function(name){
  planet <- list(name = name, structure = "rocky")
  class(planet) <- c("gas_planet", "planet")
  return(planet)
}

#' Say hello to a gas planet
#'
#' @inheritParams pkgB::hello
#'
#' @import pkgB
#'
#' @examples
#' planet <- set_gas_planet("Jupiter")
#' hello(planet)
#'
#' @export
hello.gas_planet <- function(planet, ...) {
  print(paste0("Hello, ", planet$name, "! You look breezy today."))
}

with NAMESPACE file

# Generated by roxygen2: do not edit by hand

S3method(hello,gas_planet)
export(set_gas_planet)
import(pkgB)

From this post, I learned that @importFrom is not needed. In any case, switching from @import pkgB to @importFrom pkgB did not solve the problem.

The solution I came up with after reading above mentionned post is adding @export hello below @export in the specification of hello.gas_planet(). This makes the error message disappear but instead, I get the warning:

> checking for missing documentation entries ... WARNING
  Undocumented code objects:
    'hello'
  All user-level objects in a package should have documentation entries.
  See chapter 'Writing R documentation files' in the 'Writing R
  Extensions' manual.

Writing a documentation for a generic function from another package appears to me as an unnecessary and possibly dangerous redundancy. What is the correct way of inheriting S3 generics from another package without getting any problems in devtools::check()?

EDIT:

For reproducing the error, I created two GitHub repositories for pkgA and pkgB. I produced the error on Windows 10 and Windows 11 using R 4.3.1 and 4.3.3, respectively. I installed both packages locally. The error occurs when using devtools::check() or after installing the package using devtools::install(). Running the examples after using devtools::load_all() works fine.


Solution

  • The issue is that you are calling the unqualified hello() function in your example. But the example is executed in the global namespace, and you didn’t attach ‘pkgB’, nor did you reexport pkgB::hello() from ‘pkgA’. So it’s not visible in the global namespace. That’s why your example fails.

    To fix this, either (import and) reexport pkgB::hello() from ‘pkgA’, or explicitly qualify hello() in the example as pkgB::hello().

    To reexport a name (such as an S3 generic) from another package, use

    #' @importFrom pkgB hello
    #' @export
    pkgB::hello
    

    This way you also don’t need a general @import pkgB directive.

    (There’s unfortunately a bug in the current version (7.3.1) of ‘roxygen2’ which generates an incorrect NAMESPACE directive for S3 methods of imported generics (namely, export(), whereas we want S3method()). To work around this, either use @exportS3Method pkgB::hello, or run devtools::document() multiple times until the NAMESPACE is no longer updated.)

    Here’s the complete code for pkgA/R/hello.R:

    #' @importFrom pkgB hello
    #' @export
    pkgB::hello
    
    #' Set gas planet
    #'
    #' @inheritParams pkgB::set_planet
    #'
    #' @return planet object
    #' @export
    set_gas_planet <- function(name) {
      planet <- list(name = name, structure = "rocky")
      class(planet) <- c("gas_planet", "planet")
      planet
    }
    
    #' Say hello to a gas planet
    #'
    #' @inheritParams pkgB::hello
    #'
    #' @examples
    #' planet <- set_gas_planet("Jupiter")
    #' hello(planet)
    #'
    #' @export
    hello.gas_planet <- function(planet, ...) {
      print(paste0("Hello, ", planet$name, "! You look breezy today."))
    }
    

    Alternatively, here’s the solution without reexporting pkgB::hello():

    #' Set gas planet
    #'
    #' @inheritParams pkgB::set_planet
    #'
    #' @return planet object
    #' @export
    set_gas_planet <- function(name) {
      planet <- list(name = name, structure = "rocky")
      class(planet) <- c("gas_planet", "planet")
      planet
    }
    
    #' Say hello to a gas planet
    #'
    #' @inheritParams pkgB::hello
    #'
    #' @examples
    #' planet <- set_gas_planet("Jupiter")
    #' pkgB::hello(planet)
    #'
    #' @importFrom pkgB hello
    #' @export
    hello.gas_planet <- function(planet, ...) {
      print(paste0("Hello, ", planet$name, "! You look breezy today."))
    }