Skip to content

Commit c33a88c

Browse files
authored
Merge pull request #4 from MilesMcBain/master
add ability for using to detect calls to using::pkg in .R and .Rmd
2 parents c4a6981 + b030052 commit c33a88c

14 files changed

+323
-5
lines changed

DESCRIPTION

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: using
22
Type: Package
33
Title: Add version constraints to library() calls
4-
Version: 0.3.0
4+
Version: 0.4.0
55
Authors@R: c(
66
person(given = "Anthony", family = "North", role = c("aut", "cre"), email = "[email protected]"),
77
person(given = "Miles", family = "McBain", role = c("aut"), email = "[email protected]"),
@@ -19,4 +19,9 @@ Depends: R (>= 3.3.0)
1919
Imports:
2020
utils,
2121
uuid,
22-
remotes
22+
remotes,
23+
knitr,
24+
stats,
25+
withr
26+
Suggests:
27+
testthat (>= 2.1.0)

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
export(detect_dependencies)
34
export(pkg)
45
export(using)

R/detect_dependencies.R

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
##' detect usage of using::pkg in a source file
2+
##'
3+
##' Intended for use when building lock files (e.g. for {renv}) from source
4+
##' files.
5+
##'
6+
##' returns a data.frame of all information in using::pkg calls with columns:
7+
##' `package`, `min_version`, `repo`. The value of a row may be NA if the
8+
##' argument was not supplied.
9+
##'
10+
##' Each result row is guaranteed to be unique, however the result data frame may
11+
##' contain near duplicates, e.g. using::pkg(janitor, min_version = "2.0.1") and
12+
##' using::pkg(janitor, min_version = "1.0.0") would each create a row in the
13+
##' result data frame if they appeared in the same file, due to differing
14+
##' min_version.
15+
##'
16+
##' Each using::pkg call is parsed based on it's literal expression and no variable
17+
##' substitution is done. So using(janitor, min_version = janitor_ver) would
18+
##' place "janitor_ver" in the min_row column of the result for this dependency.
19+
##'
20+
##' @title detect_dependencies
21+
##' @param file_path a length 1 character vector file path to an .R or .Rmd file
22+
##' @return a data.frame summarising found using::pkg calls in the supplied file.
23+
##' @author Miles McBain
24+
##' @export
25+
detect_dependencies <- function(file_path) {
26+
if (length(file_path) > 1) stop("file_path must be single file path not a vector of length > 1")
27+
28+
if (!file.exists(file_path)) stop("could not find file ", file_path)
29+
30+
file_type <-
31+
tolower(
32+
regmatches(
33+
file_path,
34+
regexpr("\\.[A-Za-z0-9]{1,3}$", file_path)
35+
)
36+
)
37+
38+
if (!(file_type %in% c(".r", ".rmd"))) stop("detect_dependencies only supported for .R and .Rmd")
39+
40+
deps <- switch(file_type,
41+
.r = parse_detect_deps(file_path, parse),
42+
.rmd = parse_detect_deps(file_path, parse_rmd),
43+
NULL
44+
)
45+
46+
deps[!duplicated(deps), ]
47+
}
48+
49+
parse_detect_deps <- function(file_path, file_parser) {
50+
syntax_tree <- tryCatch(file_parser(file = file_path),
51+
error = function(e) stop("Could not detect usage of using::pkg in due to invalid R code. The parser returned: \n", e$message)
52+
)
53+
54+
get_using(syntax_tree)
55+
}
56+
57+
parse_rmd <- function(file_path) {
58+
R_temp <- tempfile(fileext = ".R")
59+
on.exit(unlink(R_temp))
60+
61+
withr::with_options(
62+
list(knitr.purl.inline = TRUE),
63+
knitr::purl(file_path,
64+
output = R_temp,
65+
quiet = TRUE
66+
)
67+
)
68+
69+
parse(file = R_temp)
70+
}
71+
72+
73+
is_using_node <- function(ast_node) {
74+
node_list <- as.list(ast_node)
75+
name_node <- as.character(node_list[[1]])
76+
77+
length(name_node) == 3 &&
78+
name_node[[1]] == "::" &&
79+
name_node[[2]] == "using" &&
80+
name_node[[3]] == "pkg"
81+
}
82+
83+
extract_using_data <- function(ast_node) {
84+
node_list <- as.list(ast_node)
85+
node_list <- node_list[-1]
86+
char_nodes <- stats::setNames(
87+
lapply(node_list, as.character),
88+
names(node_list)
89+
)
90+
91+
92+
do.call(
93+
function(package = NA_character_,
94+
min_version = NA_character_,
95+
repo = NA_character_) {
96+
data.frame(
97+
package = package,
98+
min_version = min_version,
99+
repo = repo,
100+
stringsAsFactors = FALSE
101+
)
102+
},
103+
char_nodes
104+
)
105+
}
106+
107+
get_using <- function(syntax_tree) {
108+
get_using_recurse <- function(syntax_tree_expr) {
109+
if (is.call(syntax_tree_expr)) {
110+
if (is_using_node(syntax_tree_expr)) {
111+
return(extract_using_data(syntax_tree_expr))
112+
} else {
113+
make_data_frame(lapply(syntax_tree_expr, get_using_recurse))
114+
}
115+
} else {
116+
return(NULL)
117+
}
118+
}
119+
120+
make_data_frame(lapply(syntax_tree, get_using_recurse))
121+
}
122+
123+
make_data_frame <- function(x) {
124+
result_dfs <- x[!unlist(lapply(x, is.null))]
125+
126+
if (length(result_dfs) > 0) {
127+
do.call(rbind, c(result_dfs, stringsAsFactors = FALSE))
128+
} else {
129+
data.frame(
130+
package = character(0),
131+
min_version = character(0),
132+
repo = character(0), stringsAsFactors = FALSE
133+
)
134+
}
135+
}

man/detect_dependencies.Rd

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/pkg.Rd

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/using.Rd

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat.R

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
library(testthat)
2+
library(using)
3+
4+
test_check("using")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
test_that("detecting using works", {
2+
3+
## test rmd
4+
rmd_deps <- detect_dependencies(test_path("test_inputs/deps_using_rmd.Rmd"))
5+
6+
expect_equal(
7+
rmd_deps$package,
8+
c("qfes", "ffdi", "datapasta", "slippymath", "rdeck"))
9+
10+
expect_equal(
11+
rmd_deps$min_version,
12+
c("0.2.1", "0.1.3", NA, "0.1.0", "0.2.5"))
13+
14+
expect_equal(
15+
rmd_deps$repo,
16+
c("https://github.com/qfes/qfes.git",
17+
"https://[email protected]/qfes/packages/_git/ffdi",
18+
"https://github.com/milesmcbain/datapasta",
19+
NA,
20+
"https://github.com/anthonynorth/rdeck"))
21+
22+
## test R
23+
r_deps <- detect_dependencies(test_path("test_inputs/deps_using.R"))
24+
25+
expect_equal(
26+
r_deps$package,
27+
c("qfes", "ffdi"))
28+
29+
## test dupes
30+
dupe_deps <- detect_dependencies(test_path("test_inputs/deps_using_dupes.R"))
31+
32+
expect_equal(
33+
dupe_deps$package,
34+
c("qfes", "ffdi", "rdeck", "ffdi", "rdeck"))
35+
36+
37+
## test none
38+
none_deps <- detect_dependencies(test_path("test_inputs/deps_vanilla.R"))
39+
40+
expect_true(nrow(none_deps) == 0)
41+
expect_equal(names(none_deps),
42+
c("package", "min_version", "repo"))
43+
44+
## test parse fail
45+
expect_error(detect_dependencies(test_path("test_inputs/deps_parse_fail.R")),
46+
"Could not detect usage of using::pkg in due to invalid R code.")
47+
48+
## test unsupported file
49+
expect_error(detect_dependencies(test_path("test_inputs/deps_md.md")),
50+
"detect_dependencies only supported for .R and .Rmd")
51+
52+
## test only 1 file path supported
53+
expect_error(detect_dependencies(c(test_path("test_inputs/deps_using.R"),
54+
"test_inputs/deps_parse_fail.R")),
55+
"file_path must be single file path not a vector of length > 1")
56+
57+
## test file not found
58+
expect_error(detect_dependencies(test_path("test_inputs/does_not_exist.R")),
59+
"could not find file")
60+
61+
})

tests/testthat/test_inputs/deps_md.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using::pkg(qfes, min_version = "0.2.1", repo = "https://github.com/qfes/qfes.git")
2+
using::pkg(ffdi, min_version = "0.1.3", repo = "https://[email protected]/qfes/packages/_git/ffdi")
3+
using::pkg(datapasta, repo = "https://github.com/milesmcbain/datapasta")
4+
withr::with_libpaths(new = "foo/path",
5+
code = using::pkg(slippymath, min_version = "0.1.0"))
6+
library(geosphere) # to get the distance between stations
7+
library(concaveman)
8+
library(english)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using(qfes min_version = "0.2.1", repo = "https://github.com/qfes/qfes.git")
2+
using(ffdi, min_version = "0.1.3", repo = "https://[email protected]/qfes/packages/_git/ffdi")
3+
library(geosphere) # to get the distance between stations
4+
library(concaveman)
5+
library(english)
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using::pkg(qfes, min_version = "0.2.1", repo = "https://github.com/qfes/qfes.git")
2+
using::pkg(ffdi, min_version = "0.1.3", repo = "https://[email protected]/qfes/packages/_git/ffdi")
3+
library(geosphere) # to get the distance between stations
4+
library(concaveman)
5+
library(english)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using::pkg(qfes, min_version = "0.2.1", repo = "https://github.com/qfes/qfes.git")
2+
using::pkg(ffdi, min_version = "0.1.3", repo = "https://[email protected]/qfes/packages/_git/ffdi")
3+
using::pkg(rdeck, min_version = "0.2.3", repo = "https://github.com/anthonynorth/rdeck.git")
4+
5+
## as above genuine dupe
6+
using::pkg(qfes, min_version = "0.2.1", repo = "https://github.com/qfes/qfes.git")
7+
8+
## different version
9+
using::pkg(ffdi, min_version = "0.1.4", repo = "https://[email protected]/qfes/packages/_git/ffdi")
10+
11+
## different repo
12+
using::pkg(rdeck, min_version = "0.2.3", repo = "c:/rdeck/")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
title: "Untitled Draft"
3+
author: "Report Author"
4+
date: "`r format(Sys.time(), '%d %B, %Y')`"
5+
output: html_document
6+
---
7+
8+
```{r setup, include=FALSE}
9+
knitr::opts_chunk$set(echo = FALSE)
10+
```
11+
12+
## Analysis
13+
14+
```{r}
15+
using::pkg(qfes, min_version = "0.2.1", repo = "https://github.com/qfes/qfes.git")
16+
using::pkg(ffdi, min_version = "0.1.3", repo = "https://[email protected]/qfes/packages/_git/ffdi")
17+
using::pkg(datapasta, repo = "https://github.com/milesmcbain/datapasta")
18+
withr::with_libpaths(new = "foo/path",
19+
code = using::pkg(slippymath, min_version = "0.1.0"))
20+
library(geosphere) # to get the distance between stations
21+
library(concaveman)
22+
library(english)
23+
```
24+
25+
26+
`r using::pkg(rdeck, min_version = "0.2.5", repo = "https://github.com/anthonynorth/rdeck")`
27+
28+
## Reproducibility
29+
30+
<details><summary>Reproducibility receipt</summary>
31+
32+
```{r}
33+
## datetime
34+
Sys.time()
35+
## repository
36+
git2r::repository()
37+
## session info
38+
sessionInfo(package = )
39+
```
40+
41+
</details>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
library(rmarkdown)
2+
library(here) # to get project root folder in Rmd
3+
library(knitr)

0 commit comments

Comments
 (0)