Site icon R-bloggers

Tame your namespace with a dash of suggests

[This article was first published on Rtask, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

You can read the original post in its original format on Rtask website by ThinkR here: Tame your namespace with a dash of suggests

We’ve all felt it, that little wave of shiver through our skin as we watch the check of our newest commit running, thinking “Where could this possibly go wrong?”.

And suddenly, the three little redeeming ticks 0 errors ✔ | 0 warnings ✔ | 0 notes ✔

Allelhuia! 🎉

We git commit the whole thing, we git push proudly our branch and we open our Pull Request (PR) with a light and perky spirit.
Ideally, it works every time.

But sometimes it doesn’t ! The Continuous Integration (CI) crashes due to a lack of hidden dependencies, even though everything seemed to be well declared. 🫠

Want to get to the bottom of this? Join me on this expedition, hunting for Imports and Suggests in our dependencies !

📦 1. A package and its CI

Here we are on an example package, it allows us to generate graphics with {ggplot2}.

On its README.md we can see the R-CMD-check badge, telling us this package has been tested thanks to the Continuous Integration (CI) of GitHub Actions.

The CI automatically launch a check() of your package, not from a local installation, but from a minimal docker environment. The steps to run are defined in the config file R-CMD-check.yaml.

Good news, the badge is green, the CI runs with no error ! ✅

💡 2. A new function

We need a new function to save a graph in several formats at once.

We add this new function to the save_plot.R file using {fusen}1, sprinkled with a little documentation in the style of {roxygen2}2, and a usage example.

Let’s take a look at the code together.

a. the documentation

#' save_plot
#'
#' @param plot ggplot A ggplot object to be saved
#' @param ext character A vector of output format, can be multiple of "png", "svg", "jpeg", "pdf"
#' @param path character A path where to save the output
#' @param filename character The filename for the output, extension will be added
#'
#' @importFrom purrr walk
#' @importFrom ggplot2 ggsave
#' @importFrom glue glue
#'
#' @return None Create output files
#'
#' @export

b. the function body

save_plot <- function(
    plot,
    ext = c("png", "jpeg", "pdf"),
    path = "graph_output",
    filename = "output") {
  ext <- match.arg(ext, several.ok = TRUE)
  # save all format
  ext %>% walk(
    \(x) ggsave(
      filename = file.path(path, glue("{filename}.{x}")),
      plot = plot,
      device = x
    )
  )
}

c. the usage example

# create temp dir
tmp_path <- tempfile(pattern = "saveplot")
dir.create(tmp_path)
ext <- c("svg", "pdf")
data <- fetch_dataset(type = "dino")
p <- plot_dataset(
  data,
  type = "ggplot",
  candy = TRUE,
  title = "the candynosaurus rex"
)
save_plot(
  plot = p,
  filename = "dino",
  ext = ext,
  path = tmp_path
)
# clean
unlink(tmp_path, recursive = TRUE)

🔍 3. Here comes the check

We’ve got the function ready, now it’s time to make sure everything’s running smoothly.

As we’re well-mannered, we’ll do it in two steps.

a. the local check

b. the CI check

🥬 4. A CI neither green nor cabbage-looking

What do you mean mistakes !? We’ve checked that it works ! Our confidence in the check command takes a hit.

Before we lose hope, let’s take a closer look.

a. the R-CMD-check

A first clue, then. It seems that the error comes from the example of our new save_plot() function.

b. the backtrace

The backtrace tells us that the error comes from outside our package, in the ggsave() function of {ggplot2}.

c. the ggsave() function

Bingo ! The error occurs when our function tries to save a graphic in svg format but can’t find the {svglite} package !

🧶 5. Rewinding the thread of dependencies

a. the {ggplot2} imports

Let’s start with these imports.

💡 When the pipeline tried to save the graphic as svg, it went all the way back to the ggplot2::ggsave() function, but didn’t find the {svglite} package.

b. what about my local check ?

How come this problem doesn’t occur during the local check?

In other words, {svglite} is not installed by default with our package. How can we solve this?

⛵️ 6. Getting {svglite} on board

A first solution to our ailing CI would be to pass {svglite} into our package’s imports.

Does everything go back to normal after that?

Bingo ! The CI is back to green ! 🎉

🤨 7. There’s something fishy going on

a. adieu ✔✔✔

We could be satisfied with this version. Except that it feels like a pebble in our shoe.

Why is that? For this:

Our triple green is gone! 😱

Adding the {svglite} dependency brings the package’s total number of mandatory dependencies (imports) up to 21.

Rightly so, the devtools::check() warns us that this is not an optimal situation for maintaining code.

CRAN advises you to pass as many dependencies as possible in the suggests section.

b. fishing for suggestions

Passing as many imports as possible in suggests is one thing, but how do we decide on the fate of each of our dependencies?

According to CRAN, we can identify as suggests the dependencies that :

Let’s take the case of our dependency on {svglite} in deptrapr::save_plot() :

Note that, with this way of doing things, no need to wait until you’ve got 21 dependencies to start sorting, right ? 🙃

Let’s say save_plot() is a minor function in our package. As with {ggplot2}, we can then pass the {svglite} import of our package into suggests.
Let’s do it the good way.

🪄 8. Passing from imports to suggests

Let’s try to migrate our dependency on {svglite} from imports to suggests.

a. a breath of roxygen

Once {svglite} has been switched to suggest, our three ticks from the local devtools::check() are back to green. Hurray! 🎉

If we stopped here, our GitHub CI would work without a hitch, as it installs by default :

Except that it’s not enough as it is.

c. avoid backlash

Why do more? To do better!

Otherwise, we’d be passing the hot potato on to the next developer ! 😈

Let’s put ourselves in the shoes of the next person who wants to use our package.
If they try to save their graphic as svg, they’ll have to rewind the backtrace up to the suggests dependencies, just as we did with {ggplot2}.

This may sound simple, but it can quickly get bogged down in the following cases:

🛟 9. A namespace put to the test

a. the requireNamespace() function

As the output of usethis::usepackage() indicates, for suggests to be successful, they must be accompanied by a requireNamespace().

This function is used to check whether or not the dependency is missing, and to decide what action to take depending on the situation.

This enables us to achieve two useful behaviors :

In our case, we get something like this:

save_plot <- function(
    plot,
    ext = c("png", "jpeg", "pdf"),
    path = "graph_output",
    filename = "output") {
  ext <- match.arg(ext, several.ok = TRUE)
  # check svglite is installed
  if (!requireNamespace("svglite", quietly = TRUE)) {
    warning(
      paste(
        "Skipping SVG export.",
        "To enable svg export with ggsave(),",
        "please install svglite : install.packages('svglite')"
      )
    )
    ext <- ext[ext != "svg"]
  }
  # save all format
  ext %>%
    walk(\(x) {
      ggsave(
        filename = file.path(path, glue("{filename}.{x}")),
        plot = plot,
        device = x
      )
    })
}

Now if {svglite} isn’t installed, save_plot() points out the solution, all without crashing!

b. a nice readme

As we’ve seen in our wanderings, {svglite} won’t install when a user tries to install our {deptrapr} package, because it’s part of the suggests.

To save our user the trouble of poking around in the DESCRIPTION to discover this dependency, we can make his life easier with our README.

We add a paragraph mentioning the use of suggests, and the possibility to install them using the dependencies = TRUE parameter during installation.

☕️ 10. Wrap-up

For peace of mind while running a check, you now have your back with the suggests & requireNamespace() combo! 😃

Another way of quickly detecting this type of error when developing a function is to associate a unit test.

Having a test for the save_plot() function would have enabled us to detect the missing dependency at the local devtools::check() step, without having to go through the CI.

We’d directly obtain an error similar to the one observed in the CI :

Two last tips for the road:


  1. So you don’t know {fusen}? Go and have a look over here 🙂↩︎

  2. a grammar for creating documentation in a snap of fingers↩︎

  3. c.f. the function’s roxygen2 documentation↩︎

  4. since version 0.4.4 of {attachment}↩︎

This post is better presented on its original ThinkR website here: Tame your namespace with a dash of suggests

To leave a comment for the author, please follow the link and comment on their blog: Rtask.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Exit mobile version