├── tests ├── testthat │ ├── file-broken.txt │ ├── dir-simple │ │ ├── simple.R │ │ └── simple.Rmd │ ├── file-ok.R │ ├── file-format.Rmd │ ├── namespace-simple │ │ └── NAMESPACE │ ├── file-partially-broken.Rmd │ ├── test-dir.R │ ├── test-namespace.R │ ├── file-ok.Rnw │ ├── test-file.R │ └── test-code.R └── testthat.R ├── .gitignore ├── inst └── examples │ ├── simple.R │ └── simple.Rmd ├── R ├── necessity-package.R ├── utils.R ├── dir.R ├── namespace.R ├── code.R └── file.R ├── .Rbuildignore ├── NAMESPACE ├── codecov.yml ├── .travis.yml ├── man ├── req_dir.Rd ├── req_namespace.Rd ├── req_code.Rd ├── req_file.Rd └── requirements-package.Rd ├── requirements.Rproj ├── DESCRIPTION └── README.md /tests/testthat/file-broken.txt: -------------------------------------------------------------------------------- 1 | 1 + 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | -------------------------------------------------------------------------------- /tests/testthat/dir-simple/simple.R: -------------------------------------------------------------------------------- 1 | library(dplyr) 2 | -------------------------------------------------------------------------------- /inst/examples/simple.R: -------------------------------------------------------------------------------- 1 | library(ggplot2) 2 | 3 | dplyr::mutate(iris) 4 | -------------------------------------------------------------------------------- /tests/testthat/file-ok.R: -------------------------------------------------------------------------------- 1 | library(x) 2 | library(y) 3 | library(z) 4 | -------------------------------------------------------------------------------- /tests/testthat/dir-simple/simple.Rmd: -------------------------------------------------------------------------------- 1 | ```{r} 2 | library(ggplot2) 3 | ``` 4 | -------------------------------------------------------------------------------- /tests/testthat/file-format.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: tools::add_datalist 3 | --- 4 | -------------------------------------------------------------------------------- /tests/testthat/namespace-simple/NAMESPACE: -------------------------------------------------------------------------------- 1 | import(x) 2 | importFrom(y,z) 3 | -------------------------------------------------------------------------------- /R/necessity-package.R: -------------------------------------------------------------------------------- 1 | #' @import rlang 2 | #' @keywords internal 3 | "_PACKAGE" 4 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^requirements\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.travis\.yml$ 4 | ^codecov\.yml$ 5 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(requirements) 3 | 4 | test_check("requirements") 5 | -------------------------------------------------------------------------------- /tests/testthat/file-partially-broken.Rmd: -------------------------------------------------------------------------------- 1 | 2 | ```{r} 3 | 1 + 4 | ``` 5 | 6 | ```{r} 7 | library(x) 8 | ``` 9 | -------------------------------------------------------------------------------- /inst/examples/simple.Rmd: -------------------------------------------------------------------------------- 1 | ```{r} 2 | library(ggplot2) 3 | ``` 4 | 5 | ```{r} 6 | dplyr::mutate(iris) 7 | ``` 8 | 9 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(req_code) 4 | export(req_file) 5 | export(req_namespace) 6 | import(rlang) 7 | -------------------------------------------------------------------------------- /tests/testthat/test-dir.R: -------------------------------------------------------------------------------- 1 | context("test-dir") 2 | 3 | test_that("finds requirements from files", { 4 | expect_equal( 5 | req_dir(test_path("dir-simple")), 6 | c("dplyr", "ggplot2", "rmarkdown") 7 | ) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/testthat/test-namespace.R: -------------------------------------------------------------------------------- 1 | context("test-namespace") 2 | 3 | test_that("can extract from simple namespace file", { 4 | expect_equal( 5 | req_namespace(test_path("namespace-simple")), 6 | c("x", "y") 7 | ) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/testthat/file-ok.Rnw: -------------------------------------------------------------------------------- 1 | \documentclass[a4paper]{article} 2 | 3 | \title{Sweave Example} 4 | 5 | \begin{document} 6 | 7 | \maketitle 8 | 9 | <<>>= 10 | library("x") 11 | @ 12 | 13 | <>= 14 | y::z 15 | @ 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 1% 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | sudo: false 5 | cache: packages 6 | 7 | r: 8 | - 3.1 9 | - 3.2 10 | - oldrel 11 | - release 12 | - devel 13 | 14 | after_success: 15 | - Rscript -e 'covr::codecov()' 16 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | flat_map_chr <- function(x, f, ...) { 2 | if (length(x) == 0) { 3 | character() 4 | } else { 5 | unlist(lapply(x, f, ...)) 6 | } 7 | } 8 | 9 | char_or_sym <- function(x) { 10 | if (is.character(x)) { 11 | x 12 | } else if (is.symbol(x)) { 13 | as.character(x) 14 | } else { 15 | character() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /R/dir.R: -------------------------------------------------------------------------------- 1 | #' Extract requirements from a directory 2 | #' 3 | #' Extracts requirements from all files in the directory and then ... 4 | #' 5 | #' @param path Path to recursively search 6 | req_dir <- function(path = ".") { 7 | files <- dir(path, recursive = TRUE, include.dirs = FALSE, full.names = TRUE) 8 | 9 | sort(unique(flat_map_chr(files, req_file))) 10 | } 11 | -------------------------------------------------------------------------------- /man/req_dir.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/dir.R 3 | \name{req_dir} 4 | \alias{req_dir} 5 | \title{Extract requirements from a directory} 6 | \usage{ 7 | req_dir(path = ".") 8 | } 9 | \arguments{ 10 | \item{path}{Path to recursively search} 11 | } 12 | \description{ 13 | Extracts requirements from all files in the directory and then ... 14 | } 15 | -------------------------------------------------------------------------------- /man/req_namespace.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/namespace.R 3 | \name{req_namespace} 4 | \alias{req_namespace} 5 | \title{Extract requirements from a `NAMESPACE` file} 6 | \usage{ 7 | req_namespace(path) 8 | } 9 | \arguments{ 10 | \item{path}{Path to directory containing namespace file.} 11 | } 12 | \description{ 13 | Looks for `imports()` and `importFrom()` directives. 14 | } 15 | -------------------------------------------------------------------------------- /requirements.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: XeLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /man/req_code.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/code.R 3 | \name{req_code} 4 | \alias{req_code} 5 | \title{Extract requirements from code} 6 | \usage{ 7 | req_code(x) 8 | } 9 | \arguments{ 10 | \item{x}{Code to examine. Supports unquoting.} 11 | } 12 | \description{ 13 | Looks for `::`, `:::`, `library()`, `require()`, `requireNamespace()`, 14 | and `loadNamespace()`. 15 | } 16 | \examples{ 17 | req_code(library("rlang")) 18 | req_code(rlang::expr()) 19 | } 20 | -------------------------------------------------------------------------------- /R/namespace.R: -------------------------------------------------------------------------------- 1 | #' Extract requirements from a `NAMESPACE` file 2 | #' 3 | #' Looks for `imports()` and `importFrom()` directives. 4 | #' 5 | #' @param path Path to directory containing namespace file. 6 | #' @export 7 | req_namespace <- function(path) { 8 | ns <- parseNamespaceFile(basename(path), dirname(path), mustExist = TRUE) 9 | imports <- ns$imports 10 | 11 | pkg_name <- function(x) { 12 | if (length(x) == 1) { 13 | x 14 | } else if (length(x) == 2) { 15 | x[[1]] 16 | } else { 17 | character() 18 | } 19 | } 20 | 21 | unique(flat_map_chr(imports, pkg_name)) 22 | } 23 | 24 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: requirements 2 | Title: Find Packages Required for Code to Run 3 | Version: 0.0.0.9000 4 | Authors@R: c( 5 | person("Hadley", "Wickham", , "hadley@rstudio.com", role = c("aut", "cre")), 6 | person("RStudio", role = "cph") 7 | ) 8 | Description: Figure out what packages are required to run a given R expression, 9 | file ('.R', '.Rmd', '.Rnw', ...), or a directory of files. Includes 10 | heuristics for common implicit dependencies. 11 | License: GPL-3 12 | URL: https://github.com/hadley/requirements 13 | BugReports: https://github.com/hadley/requirements/issues 14 | Imports: 15 | rlang 16 | Suggests: 17 | testthat, 18 | covr, 19 | rmarkdown 20 | ByteCompile: true 21 | Encoding: UTF-8 22 | LazyData: true 23 | RoxygenNote: 6.0.1 24 | Depends: 25 | R (>= 3.1) 26 | -------------------------------------------------------------------------------- /man/req_file.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/file.R 3 | \name{req_file} 4 | \alias{req_file} 5 | \title{Extract requirements from a file} 6 | \usage{ 7 | req_file(path) 8 | } 9 | \arguments{ 10 | \item{path}{Path to file} 11 | } 12 | \description{ 13 | * `.R`: extracts requirements from parsed code 14 | 15 | * `.Rmd` & `.Rpres`: requirements from chunks (parsed using regular 16 | expressions to avoid dependency on knitr package). If rmarkdown package is 17 | installed, will also add requirements from custom output type. 18 | 19 | * `.Rnw`: tangles the document and then extracts from `.R` file. 20 | } 21 | \examples{ 22 | path_r <- system.file("examples", "simple.R", package = "requirements") 23 | path_rmd <- system.file("examples", "simple.Rmd", package = "requirements") 24 | 25 | req_file(path_r) 26 | req_file(path_rmd) 27 | } 28 | -------------------------------------------------------------------------------- /man/requirements-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/necessity-package.R 3 | \docType{package} 4 | \name{requirements-package} 5 | \alias{requirements} 6 | \alias{requirements-package} 7 | \title{requirements: Find Packages Required for Code to Run} 8 | \description{ 9 | Figure out what packages are required to run a given R expression, 10 | file ('.R', '.Rmd', '.Rnw', ...), or a directory of files. Includes 11 | heuristics for common implicit dependencies. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://github.com/hadley/requirements} 17 | \item Report bugs at \url{https://github.com/hadley/requirements/issues} 18 | } 19 | 20 | } 21 | \author{ 22 | \strong{Maintainer}: Hadley Wickham \email{hadley@rstudio.com} 23 | 24 | Other contributors: 25 | \itemize{ 26 | \item RStudio [copyright holder] 27 | } 28 | 29 | } 30 | \keyword{internal} 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # requirements 2 | 3 | [![Travis build status](https://travis-ci.org/hadley/requirements.svg?branch=master)](https://travis-ci.org/hadley/requirements) 4 | [![Coverage status](https://codecov.io/gh/hadley/requirements/branch/master/graph/badge.svg)](https://codecov.io/github/hadley/requirements?branch=master) 5 | [![CRAN status](https://www.r-pkg.org/badges/version/requirements)](https://cran.r-project.org/package=requirements) 6 | 7 | requirements allows you to impute required packages from R code, files (like `.R`, `.Rmd` and `.Rnw`) and directories. It primarily looks for use of functions like `library()` and `::`, but also uses heuristics to detect common implicit dependencies (e.g. methods, shiny, and roxygen2). 8 | 9 | The inspiration for this code comes from packrat. We're extracting it out in to a separate package so that it can be more easily extended and used by others. 10 | 11 | ## Installation 12 | 13 | requirements is not currently available on CRAN, but you can install the development version from [GitHub](https://github.com/) with: 14 | 15 | ``` r 16 | # install.packages("devtools") 17 | devtools::install_github("hadley/requirements") 18 | ``` 19 | -------------------------------------------------------------------------------- /tests/testthat/test-file.R: -------------------------------------------------------------------------------- 1 | context("test-file.R") 2 | 3 | # .R ---------------------------------------------------------------------- 4 | 5 | test_that("can extract requirements from .R file", { 6 | expect_equal(req_file(test_path("file-ok.R")), c("x", "y", "z")) 7 | }) 8 | 9 | test_that("unparseable file requires nothing ", { 10 | expect_equal(req_file_r(test_path("file-broken.txt")), character()) 11 | }) 12 | 13 | test_that("fails if path does not exist", { 14 | expect_error(req_file("DNE.r"), "does not exist") 15 | }) 16 | 17 | # .Rmd -------------------------------------------------------------------- 18 | 19 | test_that("inspects output_format", { 20 | skip_if_not_installed("rmarkdown") 21 | 22 | expect_equal( 23 | req_file(test_path("file-format.Rmd")), 24 | c("rmarkdown", "tools") 25 | ) 26 | 27 | }) 28 | 29 | test_that("unparsed chunks are ignored", { 30 | expect_equal( 31 | req_file(test_path("file-partially-broken.Rmd")), 32 | c("rmarkdown", "x") 33 | ) 34 | }) 35 | 36 | # .Rnw -------------------------------------------------------------------- 37 | 38 | test_that("basic Rnw extraction works", { 39 | expect_equal( 40 | req_file(test_path("file-ok.Rnw")), 41 | "x" 42 | ) 43 | }) 44 | -------------------------------------------------------------------------------- /R/code.R: -------------------------------------------------------------------------------- 1 | #' Extract requirements from code 2 | #' 3 | #' Looks for `::`, `:::`, `library()`, `require()`, `requireNamespace()`, 4 | #' and `loadNamespace()`. 5 | #' 6 | #' @param x Code to examine. Supports unquoting. 7 | #' @export 8 | #' @examples 9 | #' req_code(library("rlang")) 10 | #' req_code(rlang::expr()) 11 | req_code <- function(x) { 12 | x <- enexpr(x) 13 | unique(find_pkgs_rec(x)) 14 | } 15 | 16 | find_pkgs_rec <- function(x) { 17 | if (is_syntactic_literal(x) || is_symbol(x)) { 18 | return(character()) 19 | } 20 | 21 | if (is_pairlist(x) || is.expression(x)) { 22 | return(flat_map_chr(as.list(x), find_pkgs_rec)) 23 | } 24 | 25 | if (is_call(x, c("::", ":::"))) { 26 | char_or_sym(x[[2]]) 27 | } else if (is_call(x, c("library", "require"))) { 28 | x <- call_standardise(x, env = baseenv()) 29 | if (isTRUE(x$character.only) || identical(x$character.only, quote(T))) { 30 | if (is.character(x$package)) { 31 | x$package 32 | } else { 33 | character() 34 | } 35 | } else { 36 | char_or_sym(x$package) 37 | } 38 | } else if (is_call(x, c("requireNamespace", "loadNamespace"))) { 39 | x <- call_standardise(x, env = baseenv()) 40 | char_or_sym(x$package) 41 | } else { 42 | flat_map_chr(as.list(x), find_pkgs_rec) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /tests/testthat/test-code.R: -------------------------------------------------------------------------------- 1 | context("test-code.R") 2 | 3 | test_that("doesn't find packages when not present", { 4 | expect_equal(req_code(1), character()) 5 | expect_equal(req_code(x), character()) 6 | expect_equal(req_code(library()), character()) 7 | expect_equal(req_code(function(x, y) {}), character()) 8 | }) 9 | 10 | test_that("finds explicit package loading calls", { 11 | expect_equal(req_code(library(x)), "x") 12 | expect_equal(req_code(library("x")), "x") 13 | expect_equal(req_code(require(x)), "x") 14 | expect_equal(req_code(requireNamespace(x)), "x") 15 | expect_equal(req_code(loadNamespace(x)), "x") 16 | }) 17 | 18 | test_that("handle character.only correctly", { 19 | expect_equal(req_code(library(x, char = TRUE)), character()) 20 | expect_equal(req_code(library(x, character.only = TRUE)), character()) 21 | expect_equal(req_code(library("x", character.only = TRUE)), "x") 22 | }) 23 | 24 | test_that("find namespace qualifiers", { 25 | expect_equal(req_code(x::foo), "x") 26 | expect_equal(req_code(x:::foo), "x") 27 | expect_equal(req_code(x::foo(x)), "x") 28 | expect_equal(req_code(x:::foo(x)), "x") 29 | }) 30 | 31 | test_that("can find multiple packages", { 32 | expect_equal(req_code({x::f; x::f}), "x") 33 | expect_equal(req_code({x::f; y::f}), c("x", "y")) 34 | expect_equal(req_code(function(x = x::f, y = y::f) {}), c("x", "y")) 35 | }) 36 | 37 | test_that("can work with expressions", { 38 | x1 <- expression() 39 | expect_equal(req_code(!!x1), character()) 40 | 41 | x2 <- expression(x::f(0), y::f(1)) 42 | expect_equal(req_code(!!x2), c("x", "y")) 43 | }) 44 | 45 | -------------------------------------------------------------------------------- /R/file.R: -------------------------------------------------------------------------------- 1 | #' Extract requirements from a file 2 | #' 3 | #' @description 4 | #' * `.R`: extracts requirements from parsed code 5 | #' 6 | #' * `.Rmd` & `.Rpres`: requirements from chunks (parsed using regular 7 | #' expressions to avoid dependency on knitr package). If rmarkdown package is 8 | #' installed, will also add requirements from custom output type. 9 | #' 10 | #' * `.Rnw`: tangles the document and then extracts from `.R` file. 11 | #' 12 | #' @param path Path to file 13 | #' @export 14 | #' @examples 15 | #' path_r <- system.file("examples", "simple.R", package = "requirements") 16 | #' path_rmd <- system.file("examples", "simple.Rmd", package = "requirements") 17 | #' 18 | #' req_file(path_r) 19 | #' req_file(path_rmd) 20 | req_file <- function(path) { 21 | if (!file.exists(path)) { 22 | stop("`path` does not exist", call. = FALSE) 23 | } 24 | ext <- tolower(tools::file_ext(path)) 25 | 26 | switch(ext, 27 | r = req_file_r(path), 28 | rmd = req_file_rmd(path), 29 | rnw = req_file_rnw(path), 30 | rpres = req_file_rmd(path), 31 | character() 32 | ) 33 | } 34 | 35 | # .R ---------------------------------------------------------------------- 36 | 37 | req_file_r <- function(path) { 38 | tryCatch( 39 | error = function(err) character(), 40 | { 41 | code <- parse(path) 42 | req_code(!!code) 43 | } 44 | ) 45 | } 46 | 47 | # .Rmd -------------------------------------------------------------------- 48 | 49 | req_file_rmd <- function(path) { 50 | lines <- readLines(path) 51 | 52 | chunks <- rmd_chunks(lines) 53 | chunk_reqs <- flat_map_chr(chunks, req_text) 54 | 55 | if (!requireNamespace("rmarkdown", quietly = TRUE)) { 56 | yaml_reqs <- character() 57 | } else { 58 | format <- rmarkdown::default_output_format(path) 59 | yaml_reqs <- req_text(format) 60 | } 61 | 62 | reqs <- c("rmarkdown", chunk_reqs, yaml_reqs) 63 | unique(reqs) 64 | } 65 | 66 | rmd_chunks <- function(lines) { 67 | # From https://github.com/rstudio/rstudio/blob/0edb05f67b4f2eea25b8cfb15f7c64ec9b27b288/src/gwt/acesupport/acemode/rmarkdown_highlight_rules.js#L181-L184 68 | chunk_start_re <- "^(?:[ ]{4})?`{3,}\\s*\\{[Rr]\\b(?:.*)engine\\s*\\=\\s*['\"][rR]['\"](?:.*)\\}\\s*$|^(?:[ ]{4})?`{3,}\\s*\\{[rR]\\b(?:.*)\\}\\s*$"; 69 | chunk_end_re <- "^(?:[ ]{4})?`{3,}\\s*$" 70 | 71 | chunk_start <- grepl(chunk_start_re, lines, perl = TRUE) 72 | chunk_end <- grepl(chunk_end_re, lines, perl = TRUE) 73 | 74 | chunk_num <- cumsum(chunk_start) 75 | in_chunk <- (chunk_num - cumsum(chunk_end)) != 0 76 | 77 | chunks <- split(lines[in_chunk], chunk_num[in_chunk]) 78 | names(chunks) <- NULL 79 | 80 | # Strip off first element, the chunk header 81 | chunks <- lapply(chunks, function(x) x[-1]) 82 | lapply(chunks, paste, collapse = "\n") 83 | } 84 | 85 | req_text <- function(text) { 86 | tryCatch( 87 | error = function(err) character(), 88 | { 89 | code <- parse(text = text) 90 | req_code(!!code) 91 | } 92 | ) 93 | } 94 | 95 | 96 | # .Rnw -------------------------------------------------------------------- 97 | 98 | req_file_rnw <- function(path) { 99 | tempfile <- tempfile() 100 | on.exit(unlink(tempfile)) 101 | 102 | utils::Stangle(path, output = tempfile, quiet = TRUE) 103 | req_file_r(tempfile) 104 | } 105 | --------------------------------------------------------------------------------