rconflicting-librariespackage-development

Print R package startup message AFTER automatic package conflict messages instead of before


My R package ale implements a get() method. Normally, this would conflict with the base R base::get() function, but I implement it such that it calls my package's custom get() methods when the user passes one of my package's objects but for everything else, it transparently hands over to base::get() so that there is no problem. So far, so good.

However, when a package is attached with library() R automatically prints a conflict message to alert users to the potential problem:

# To reproduce this issue, you need to install the current 
# development version of the package at this specific commit:
# # install.packages('pak')
# pak::pak('tripartio/ale@e0ff702')
# 
library(ale)
#> 
#> Attaching package: 'ale'
#> The following object is masked from 'package:base':
#> 
#>     get

Created on 2025-03-03 with reprex v2.1.1

This is fine and responsible on R's part (my question is NOT about trying to suppress this message--I leave that to the user's choice). However, I have added a clarifying package startup message to reassure users that there is no problem. I have the following .onAttach() function in my aaa.R file:

# My actual .onAttach() is more complicated, but I simplify here
# to focus on the key issues
.onAttach <- function(libname, pkgname) {
  packageStartupMessage("The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.")
}

The result when users load the package now is:

# Note: you must first restart R before each time you run library(ale) to see what any changes produce
library(ale)
#> The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.
#> 
#> Attaching package: 'ale'
#> The following object is masked from 'package:base':
#> 
#>     get

Created on 2025-03-03 with reprex v2.1.1

This is what I want, except I want to print my custom message AFTER R's conflict message, not before. I've tried many things but I have not been able to successfully flip the order. For example, here are some things I've tried that have NOT worked:

# Delay the message using setHook()
.onAttach <- function(libname, pkgname) {
  setHook(
    packageEvent(pkgname, "attach"),
    function(...) {
      packageStartupMessage("The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.")
    }
  )
}

# Use defer() from {withr}
.onAttach <- function(libname, pkgname) {
  withr::defer({
    packageStartupMessage("The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.")
  }, envir = parent.env(environment()))
}

# Sys.sleep() before my custom message:
# 5 seconds delay but same order
.onAttach <- function(libname, pkgname) {
  Sys.sleep(5)
  packageStartupMessage("The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.")
}

# .onLoad() instead of .onAttach():
# No change--same order
.onLoad <- function(libname, pkgname) {
  setHook(
    packageEvent(pkgname, "attach"),
    function(...) {
      packageStartupMessage("The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.")
    }
  )
}

Here is the output that I want; I appreciate any help in getting me there:

# Note: you must first restart R before each time you run library(ale) to see what any changes produce
library(ale)
#> 
#> Attaching package: 'ale'
#> The following object is masked from 'package:base':
#> 
#>     get
#>
#> The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.


Solution

  • You could try to take a creative approach by using R's task callback system to print the packageStartupMessage after .onAttach ran. I am writing this answer because @Tripartio asked me to do so in the comments and this turned out to be working.

    addTaskCallback registers an R function that is to be called each time a top-level task is completed.

    If the (return)value is FALSE, the callback is removed from the task list and will not be called again by this mechanism

    .onAttach <- function(libname, pkgname) {
      # Schedule the message to appear after the current task is complete
      invisible(
        addTaskCallback(function(expr, value, ok, visible) {
          packageStartupMessage("\nNote: The 'get()' function for 'ale' works such that the masked base::get() still works fine without modification.")     
          return(FALSE) # to remove this callback after it runs once
        })
      )
    }
    

    DISCLAIMER:

    I found out, that all startup messages need to be suppressable to fit CRAN guidelines. I do not know, if this solution fits these guidelines, but since this also outputs a type packageStartupMessage you should be fine. Source