Do You have Stale Imports or Suggests?
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.
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.
'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.
[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.
[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
}
}
- Note 1: We ought to check if there are any imports at all. It would be a bit unusual, but it’s possible to have zero imports.
- Note 2:
g_imp
contains the results of the grep process. If there are imports in the package, each imported package name will be found by grep in theDESCRIPTION
file. That’s not so interesting, so we don’t count it. For a package to be stale, it will be found inDESCRIPTION
but no where else.
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
Reuse
Citation
@online{hanson2022,
author = {Hanson, Bryan},
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}
}