Do You have Stale Imports or Suggests?

R
Utilities
Developers
Hints For Package Developers
Author

Bryan Hanson

Published

February 9, 2022

I’ve been developing packages in R for over a decade now. When adding new features to a package, I often import functions from another package, and of course that package goes in the Imports: field of the DESCRIPTION file. Later, I might change my approach entirely and no longer need that package. Do I remember to remove it from DESCRIPTION? Generally not. The same thing happens when writing a new vignette, and it can happen with the Suggests: field as well. It can also happen when one splits a packages into several smaller packages. If one forgets to delete a package from the DESCRIPTION file, the dependencies become bloated, because all the imported and suggested packages have to be available to install the package. This adds overhead to the project, and increases the possibility of a namespace conflict.

In fact this just happened to me again! The author of a package I had in Suggests: wrote to me and let me know their package would be archived. It was an easy enough fix for me, as it was a “stale” package in that I was no longer using it. I had added it for a vignette which I later deleted, as I decided a series of blog posts was a better approach.

So I decided to write a little function to check for such stale Suggests: and Import: entries. This post is about that function. As far as I can tell there is no built-in function for this purpose, and CRAN does not check for stale entries. So it was worth my time to automate the process.1

The first step is to read in the DESCRIPTION file for the package (so we want our working directory to be the top level of the package). There is a built in function for this. We’ll use the DESCRIPTION file from the ChemoSpec package as a demonstration.

# setwd("...") # set to the top level of the package
desc <- read.dcf("DESCRIPTION", all = TRUE)

The argument all = TRUE is a bit odd in that it has a particular purpose (see ?read.dcf) which isn’t really important here, but has the side effect of returning a data frame, which makes our job simpler. Let’s look at what is returned.

str(desc)
'data.frame':   1 obs. of  18 variables:
 $ Package         : chr "ChemoSpec"
 $ Type            : chr "Package"
 $ Title           : chr "Exploratory Chemometrics for Spectroscopy"
 $ Version         : chr "6.1.2"
 $ Date            : chr "2022-02-08"
 $ Authors@R       : chr "c(\nperson(\"Bryan A.\", \"Hanson\",\nrole = c(\"aut\", \"cre\"), email =\n\"hanson@depauw.edu\",\ncomment = c("| __truncated__
 $ Description     : chr "A collection of functions for top-down exploratory data analysis\nof spectral data including nuclear magnetic r"| __truncated__
 $ License         : chr "GPL-3"
 $ Depends         : chr "R (>= 3.5),\nChemoSpecUtils (>= 1.0)"
 $ Imports         : chr "plyr,\nstats,\nutils,\ngrDevices,\nreshape2,\nreadJDX (>= 0.6),\npatchwork,\nggplot2,\nplotly,\nmagrittr"
 $ Suggests        : chr "IDPmisc,\nknitr,\njs,\nNbClust,\nlattice,\nbaseline,\nmclust,\npls,\nclusterCrit,\nR.utils,\nRColorBrewer,\nser"| __truncated__
 $ URL             : chr "https://bryanhanson.github.io/ChemoSpec/"
 $ BugReports      : chr "https://github.com/bryanhanson/ChemoSpec/issues"
 $ ByteCompile     : chr "TRUE"
 $ VignetteBuilder : chr "knitr"
 $ Encoding        : chr "UTF-8"
 $ RoxygenNote     : chr "7.1.2"
 $ NeedsCompilation: chr "no"

We are interested in the Imports and Suggests elements. Let’s look more closely.

head(desc$Imports)
[1] "plyr,\nstats,\nutils,\ngrDevices,\nreshape2,\nreadJDX (>= 0.6),\npatchwork,\nggplot2,\nplotly,\nmagrittr"

You can see there are a bunch of newlines in there (\n), along with some version specifications, in parentheses. We need to clean this up so we have a simple list of the packages as a vector. For clean up we’ll use the following helper function.

clean_up <- function(string) {
  string <- gsub("\n", "", string) # remove newlines
  string <- gsub("\\(.+\\)", "", string) # remove parens & anything within them
  string <- unlist(strsplit(string, ",")) # split the long string into pieces
  string <- trimws(string) # remove any white space around words
}

After we apply this to the raw results, we have what we are after, a clean list of imported packages.

imp <- clean_up(desc$Imports)
imp
 [1] "plyr"      "stats"     "utils"     "grDevices" "reshape2"  "readJDX"  
 [7] "patchwork" "ggplot2"   "plotly"    "magrittr" 

Next, we can search the entire package looking for these package names to see if they are used in the package. They might appear in import statements, vignettes, code and so forth, so it’s not sufficient to just look at code. This is a job for grep, but we’ll call grep from within R so that we don’t have to use the command line and transfer the results to R, that gets messy and is error-prone.

if (length(imp) >= 1) { # Note 1
  imp_res <- rep("FALSE", length(imp)) # Boolean to keep track of whether we found a package or not
  for (i in 1:length(imp)) {
    args <- paste("-r -e '", imp[i], "' *", sep = "") # assemble arguments for grep
    g_imp <- system2("grep", args, stdout = TRUE)
    if (length(g_imp) > 1L) imp_res[i] <- TRUE # Note 2
  }
}

We can do the same process for the Suggests: field of DESCRIPTION. And then it would be nice to present the results in a more useable form. At this point we can put it all togther in an easy-to-use function.2

# run from the package top level
check_stale_imports_suggests <- function() {

  # helper function: removes extra characters
  # from strings read by read.dcf
  clean_up <- function(string) {
    string <- gsub("\n", "", string)
    string <- gsub("\\(.+\\)", "", string)
    string <- unlist(strsplit(string, ","))
    string <- trimws(string)
  }

  desc <- read.dcf("DESCRIPTION", all = TRUE)

  # look for use of imported packages
  imp <- clean_up(desc$Imports)
  if (length(imp) == 0L) message("No Imports: entries found")
  if (length(imp) >= 1) {
    imp_res <- rep("FALSE", length(imp))
    for (i in 1:length(imp)) {
      args <- paste("-r -e '", imp[i], "' *", sep = "")
      g_imp <- system2("grep", args, stdout = TRUE)
      # always found once in DESCRIPTION, hence > 1
      if (length(g_imp) > 1L) imp_res[i] <- TRUE
    }
  }

  # look for use of suggested packages
  sug <- clean_up(desc$Suggests)
  if (length(sug) == 0L) message("No Suggests: entries found")
  if (length(sug) >= 1) {
    sug_res <- rep("FALSE", length(sug))
    for (i in 1:length(sug)) {
      args <- paste("-r -e '", sug[i], "' *", sep = "")
      g_sug <- system2("grep", args, stdout = TRUE)
      # always found once in DESCRIPTION, hence > 1
      if (length(g_sug) > 1L) sug_res[i] <- TRUE
    }
  }

  # arrange output in easy to read format
  role <- c(rep("Imports", length(imp)), rep("Suggests", length(sug)))

  return(data.frame(
    pkg = c(imp, sug),
    role = role,
    found = c(imp_res, sug_res)))
}

Applying this function to my ChemoSpec2D package (as of the date of this post), we see the following output. You can see a bunch of packages are imported but never used, so I have some work to do. This was the result of copying the DESCRIPTION file from ChemoSpec when I started ChemoSpec2D and obviously I never went back and cleaned things up.

            pkg     role found
1          plyr  Imports  TRUE
2         stats  Imports  TRUE
3         utils  Imports  TRUE
4     grDevices  Imports  TRUE
5      reshape2  Imports  TRUE
6       readJDX  Imports  TRUE
7     patchwork  Imports  TRUE
8       ggplot2  Imports  TRUE
9        plotly  Imports  TRUE
10     magrittr  Imports  TRUE
11      IDPmisc Suggests  TRUE
12        knitr Suggests  TRUE
13           js Suggests  TRUE
14      NbClust Suggests  TRUE
15      lattice Suggests  TRUE
16     baseline Suggests  TRUE
17       mclust Suggests  TRUE
18          pls Suggests  TRUE
19  clusterCrit Suggests  TRUE
20      R.utils Suggests  TRUE
21 RColorBrewer Suggests  TRUE
22    seriation Suggests FALSE
23         MASS Suggests FALSE
24   robustbase Suggests FALSE
25         grid Suggests  TRUE
26        pcaPP Suggests FALSE
27     jsonlite Suggests FALSE
28       gsubfn Suggests FALSE
29       signal Suggests  TRUE
30        speaq Suggests FALSE
31     tinytest Suggests FALSE
32   elasticnet Suggests FALSE
33        irlba Suggests FALSE
34         amap Suggests FALSE
35    rmarkdown Suggests  TRUE
36     bookdown Suggests FALSE
37 chemometrics Suggests FALSE
38    hyperSpec Suggests FALSE

Footnotes

  1. As you will see in a moment, during testing I found a bunch of stale entries I need to remove from several packages!↩︎

  2. In easy to use form as a Gist.↩︎

Reuse

Citation

BibTeX citation:
@online{hanson2022,
  author = {Bryan Hanson},
  title = {Do {You} Have {Stale} {Imports} or {Suggests?}},
  date = {2022-02-09},
  url = {http://chemospec.org/posts/2022-02-09-Imports-Suggests/2022-02-09-Imports-Suggests.html},
  langid = {en}
}
For attribution, please cite this work as:
Bryan Hanson. 2022. “Do You Have Stale Imports or Suggests?” February 9, 2022. http://chemospec.org/posts/2022-02-09-Imports-Suggests/2022-02-09-Imports-Suggests.html.