From 667ba05a02ebd3a88d55b99df0fc375ba9c61e51 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Tue, 7 Mar 2023 14:09:02 +0100 Subject: [PATCH 1/5] Return list from `standalone_dependencies()` --- R/use-standalone.R | 6 ++++-- tests/testthat/test-use-standalone.R | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/R/use-standalone.R b/R/use-standalone.R index 6e442417c..ab7f7c76b 100644 --- a/R/use-standalone.R +++ b/R/use-standalone.R @@ -42,7 +42,7 @@ use_standalone <- function(repo_spec, file = NULL, ref = NULL, host = NULL) { write_over(proj_path(dest_path), lines, overwrite = TRUE) dependencies <- standalone_dependencies(lines, path) - for (dependency in dependencies) { + for (dependency in dependencies$deps) { use_standalone(repo_spec, dependency) } @@ -121,5 +121,7 @@ standalone_dependencies <- function(lines, path, error_call = caller_env()) { call = error_call ) } - deps %||% character() + deps <- deps %||% character() + + list(deps = deps) } diff --git a/tests/testthat/test-use-standalone.R b/tests/testthat/test-use-standalone.R index 57c0fcc98..fcefa3b41 100644 --- a/tests/testthat/test-use-standalone.R +++ b/tests/testthat/test-use-standalone.R @@ -40,7 +40,8 @@ test_that("header provides useful summary", { test_that("can extract dependencies", { extract_deps <- function(deps) { - standalone_dependencies(c("# ---", deps, "# ---"), "test.R") + out <- standalone_dependencies(c("# ---", deps, "# ---"), "test.R") + out$deps } expect_equal(extract_deps(NULL), character()) From f3260bfddc081f665f5ac92eea351417c269904f Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Tue, 7 Mar 2023 14:15:27 +0100 Subject: [PATCH 2/5] Add support for `imports:` field in standalone files --- R/use-standalone.R | 104 ++++++++++++++++++++++-- R/utils.R | 5 ++ man/use_standalone.Rd | 17 ++++ tests/testthat/_snaps/use-standalone.md | 20 +++++ tests/testthat/test-use-standalone.R | 44 +++++++++- 5 files changed, 184 insertions(+), 6 deletions(-) diff --git a/R/use-standalone.R b/R/use-standalone.R index ab7f7c76b..a810c5565 100644 --- a/R/use-standalone.R +++ b/R/use-standalone.R @@ -8,6 +8,21 @@ #' It always overwrites an existing standalone file of the same name, making #' it easy to update previously imported code. #' +#' @section Supported fields: +#' +#' - `dependencies`: A file or a list of files in the same repo that +#' the standalone file depends on. These files are retrieved +#' automatically by `use_standalone()`. +#' +#' - `imports`: A package or list of packages that the standalone file +#' depends on. A minimal version may be specified in parentheses, +#' e.g. `rlang (>= 1.0.0)`. These dependencies are passed to +#' [use_package()] to ensure they are included in the `Imports:` +#' field of the `DESCRIPTION` file. +#' +#' Note that lists are specified with standard YAML syntax, using +#' square brackets. +#' #' @inheritParams create_from_github #' @inheritParams use_github_file #' @param file Name of standalone file. The `standalone-` prefix and file @@ -42,10 +57,24 @@ use_standalone <- function(repo_spec, file = NULL, ref = NULL, host = NULL) { write_over(proj_path(dest_path), lines, overwrite = TRUE) dependencies <- standalone_dependencies(lines, path) + for (dependency in dependencies$deps) { use_standalone(repo_spec, dependency) } + imports <- dependencies$imports + + for (i in seq_len(nrow(imports))) { + import <- imports[i, , drop = FALSE] + + if (is.na(import$ver)) { + ver <- NULL + } else { + ver <- import$ver + } + use_package(import$pkg, min_version = ver) + } + invisible() } @@ -114,14 +143,79 @@ standalone_dependencies <- function(lines, path, error_call = caller_env()) { temp <- withr::local_tempfile(lines = header) yaml <- rmarkdown::yaml_front_matter(temp) - deps <- yaml$dependencies - if (!is.null(deps) && !is.character(deps)) { + as_chr_field <- function(field) { + if (!is.null(field) && !is.character(field)) { + cli::cli_abort( + "Invalid dependencies specification in {.path {path}}.", + call = error_call + ) + } + + field %||% character() + } + + deps <- as_chr_field(yaml$dependencies) + imports <- as_chr_field(yaml$imports) + imports <- as_version_info(imports, error_call = error_call) + + if (any(na.omit(imports$cmp) != ">=")) { cli::cli_abort( - "Invalid dependencies specification in {.path {path}}.", + "Version specification must use {.code >=}.", call = error_call ) } - deps <- deps %||% character() - list(deps = deps) + list(deps = deps, imports = imports) +} + +as_version_info <- function(fields, error_call = caller_env()) { + if (!length(fields)) { + return(version_info_df()) + } + + if (any(grepl(",", fields))) { + msg <- c( + "Version field can't contain comma.", + "i" = "Do you need to wrap in a list?" + ) + cli::cli_abort(msg, call = error_call) + } + + info <- lapply(fields, as_version_info_row, error_call = error_call) + inject(rbind(!!!info)) +} + +as_version_info_row <- function(field, error_call = caller_env()) { + version_regex <- "(.*) \\((.*)\\)$" + has_ver <- grepl(version_regex, field) + + if (!has_ver) { + return(version_info_df(field, NA, NA)) + } + + pkg <- sub(version_regex, "\\1", field) + ver <- sub(version_regex, "\\2", field) + + ver <- strsplit(ver, " ")[[1]] + + if (!is_character(ver, n = 2) || any(is.na(ver)) || !all(nzchar(ver))) { + cli::cli_abort( + c( + "Can't parse version `{field}` in `imports:` field.", + "i" = "Example of expected version format: `rlang (>= 1.0.0)`." + ), + call = error_call + ) + } + + version_info_df(pkg, ver[[1]], ver[[2]]) +} + +version_info_df <- function(pkg = chr(), cmp = chr(), ver = chr()) { + df <- data.frame( + pkg = as.character(pkg), + cmp = as.character(cmp), + ver = as.character(ver) + ) + structure(df, class = c("tbl", "data.frame")) } diff --git a/R/utils.R b/R/utils.R index 38a60011d..ad1814f55 100644 --- a/R/utils.R +++ b/R/utils.R @@ -114,3 +114,8 @@ maybe_string <- function(x, nm = deparse(substitute(x))) { check_string(x, nm = nm) } } + +# For stability of `stringsAsFactors` across versions +data.frame <- function(..., stringsAsFactors = FALSE) { + base::data.frame(..., stringsAsFactors = stringsAsFactors) +} diff --git a/man/use_standalone.Rd b/man/use_standalone.Rd index 198178e42..bb3e61010 100644 --- a/man/use_standalone.Rd +++ b/man/use_standalone.Rd @@ -42,3 +42,20 @@ to get such a file into your own repo. It always overwrites an existing standalone file of the same name, making it easy to update previously imported code. } +\section{Supported fields}{ + +\itemize{ +\item \code{dependencies}: A file or a list of files in the same repo that +the standalone file depends on. These files are retrieved +automatically by \code{use_standalone()}. +\item \code{imports}: A package or list of packages that the standalone file +depends on. A minimal version may be specified in parentheses, +e.g. \verb{rlang (>= 1.0.0)}. These dependencies are passed to +\code{\link[=use_package]{use_package()}} to ensure they are included in the \verb{Imports:} +field of the \code{DESCRIPTION} file. +} + +Note that lists are specified with standard YAML syntax, using +square brackets. +} + diff --git a/tests/testthat/_snaps/use-standalone.md b/tests/testthat/_snaps/use-standalone.md index 979e29766..9b1e3532e 100644 --- a/tests/testthat/_snaps/use-standalone.md +++ b/tests/testthat/_snaps/use-standalone.md @@ -22,6 +22,26 @@ [3] "# ----------------------------------------------------------------------" [4] "#" +# can extract imports + + Code + extract_imports("# imports: rlang (== 1.0.0)") + Condition + Error in `extract_imports()`: + ! Version specification must use `>=`. + Code + extract_imports("# imports: rlang (>= 1.0.0), purrr") + Condition + Error in `extract_imports()`: + ! Version field can't contain comma. + i Do you need to wrap in a list? + Code + extract_imports("# imports: foo (>=0.0.0)") + Condition + Error in `extract_imports()`: + ! Can't parse version `foo (>=0.0.0)` in `imports:` field. + i Example of expected version format: `rlang (>= 1.0.0)`. + # errors on malformed dependencies Code diff --git a/tests/testthat/test-use-standalone.R b/tests/testthat/test-use-standalone.R index fcefa3b41..d40dbf644 100644 --- a/tests/testthat/test-use-standalone.R +++ b/tests/testthat/test-use-standalone.R @@ -2,11 +2,16 @@ test_that("can import standalone file with dependencies", { skip_if_offline() create_local_package() - use_standalone("r-lib/rlang", "types-check", ref = "4670cb233ecc8d11") + # NOTE: Check ref after r-lib/rlang@standalone-dep has been merged + use_standalone("r-lib/rlang", "types-check", ref = "73182fe94") expect_setequal( as.character(path_rel(dir_ls(proj_path("R"))), proj_path()), c("R/import-standalone-types-check.R", "R/import-standalone-obj-type.R") ) + + desc <- proj_desc() + imports <- proj_desc()$get_field("Imports") + expect_true(grepl("rlang \\(", imports)) }) test_that("can use full github url", { @@ -49,6 +54,43 @@ test_that("can extract dependencies", { expect_equal(extract_deps("# dependencies: [a, b]"), c("a", "b")) }) +test_that("can extract imports", { + extract_imports <- function(imports) { + out <- standalone_dependencies( + c("# ---", imports, "# ---"), + "test.R", + error_call = current_env() + ) + out$imports + } + + expect_equal( + extract_imports(NULL), + version_info_df() + ) + + expect_equal( + extract_imports("# imports: rlang"), + version_info_df("rlang", NA, NA) + ) + + expect_equal( + extract_imports("# imports: rlang (>= 1.0.0)"), + version_info_df("rlang", ">=", "1.0.0") + ) + + expect_equal( + extract_imports("# imports: [rlang (>= 1.0.0), purrr]"), + version_info_df(c("rlang", "purrr"), c(">=", NA), c("1.0.0", NA)) + ) + + expect_snapshot(error = TRUE, { + extract_imports("# imports: rlang (== 1.0.0)") + extract_imports("# imports: rlang (>= 1.0.0), purrr") + extract_imports("# imports: foo (>=0.0.0)") + }) +}) + test_that("errors on malformed dependencies", { expect_snapshot(error = TRUE, { standalone_dependencies(c(), "test.R") From e1976a2d0af5faca02a6dbe1266fa09fbd30221c Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Tue, 7 Mar 2023 17:59:57 +0100 Subject: [PATCH 3/5] Pass `ref` and `host` on recursion --- R/use-standalone.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/use-standalone.R b/R/use-standalone.R index a810c5565..ef113b226 100644 --- a/R/use-standalone.R +++ b/R/use-standalone.R @@ -59,7 +59,7 @@ use_standalone <- function(repo_spec, file = NULL, ref = NULL, host = NULL) { dependencies <- standalone_dependencies(lines, path) for (dependency in dependencies$deps) { - use_standalone(repo_spec, dependency) + use_standalone(repo_spec, dependency, ref = ref, host = host) } imports <- dependencies$imports From 855812de5152b6ab546c78e0b66c828ae34a9749 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Wed, 8 Mar 2023 07:40:38 -0800 Subject: [PATCH 4/5] Add some documentation --- R/use-standalone.R | 24 +++++++++++++++++++++- man/use_standalone.Rd | 48 +++++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/R/use-standalone.R b/R/use-standalone.R index ef113b226..9cd43c717 100644 --- a/R/use-standalone.R +++ b/R/use-standalone.R @@ -10,6 +10,23 @@ #' #' @section Supported fields: #' + +#' A standalone file has YAML frontmatter that provides additional information, +#' such as where the file originates from and when it was last updated. Here is +#' an example: +#' +#' ``` +#' --- +#' repo: r-lib/rlang +#' file: standalone-types-check.R +#' last-updated: 2023-03-07 +#' license: https://unlicense.org +#' dependencies: standalone-obj-type.R +#' imports: rlang (>= 1.1.0) +#' --- +#' +#' Two of these fields are consulted by `use_standalone()`: +#' #' - `dependencies`: A file or a list of files in the same repo that #' the standalone file depends on. These files are retrieved #' automatically by `use_standalone()`. @@ -21,7 +38,7 @@ #' field of the `DESCRIPTION` file. #' #' Note that lists are specified with standard YAML syntax, using -#' square brackets. +#' square brackets, for example: `# imports: [rlang (>= 1.0.0), purrr]`. #' #' @inheritParams create_from_github #' @inheritParams use_github_file @@ -29,6 +46,11 @@ #' extension are optional. If omitted, will allow you to choose from the #' standalone files offered by that repo. #' @export +#' @examples +#' \dontrun{ +#' use_standalone("r-lib/rlang", file = "types-check") +#' use_standalone("r-lib/rlang", file = "types-check", ref = "standalone-dep") +#' } use_standalone <- function(repo_spec, file = NULL, ref = NULL, host = NULL) { check_is_project() diff --git a/man/use_standalone.Rd b/man/use_standalone.Rd index bb3e61010..8db7ece6d 100644 --- a/man/use_standalone.Rd +++ b/man/use_standalone.Rd @@ -44,18 +44,44 @@ it easy to update previously imported code. } \section{Supported fields}{ -\itemize{ -\item \code{dependencies}: A file or a list of files in the same repo that -the standalone file depends on. These files are retrieved -automatically by \code{use_standalone()}. -\item \code{imports}: A package or list of packages that the standalone file -depends on. A minimal version may be specified in parentheses, -e.g. \verb{rlang (>= 1.0.0)}. These dependencies are passed to -\code{\link[=use_package]{use_package()}} to ensure they are included in the \verb{Imports:} -field of the \code{DESCRIPTION} file. -} + +A standalone file has YAML frontmatter that provides additional information, +such as where the file originates from and when it was last updated. Here is +an example: + +\if{html}{\out{
}}\preformatted{--- +repo: r-lib/rlang +file: standalone-types-check.R +last-updated: 2023-03-07 +license: https://unlicense.org +dependencies: standalone-obj-type.R +imports: rlang (>= 1.1.0) +--- + +Two of these fields are consulted by `use_standalone()`: + +- `dependencies`: A file or a list of files in the same repo that + the standalone file depends on. These files are retrieved + automatically by `use_standalone()`. + +- `imports`: A package or list of packages that the standalone file + depends on. A minimal version may be specified in parentheses, + e.g. `rlang (>= 1.0.0)`. These dependencies are passed to + [use_package()] to ensure they are included in the `Imports:` + field of the `DESCRIPTION` file. Note that lists are specified with standard YAML syntax, using -square brackets. +square brackets, for example: `# imports: [rlang (>= 1.0.0), purrr]`. + + +[use_package()]: R:use_package() +[rlang (>= 1.0.0), purrr]: R:rlang\%20(\%3E=\%201.0.0),\%20purrr +}\if{html}{\out{
}} } +\examples{ +\dontrun{ +use_standalone("r-lib/rlang", file = "types-check") +use_standalone("r-lib/rlang", file = "types-check", ref = "standalone-dep") +} +} From 89806af981c38b8d982af1077f04afb8f4cfb49e Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Wed, 8 Mar 2023 07:45:09 -0800 Subject: [PATCH 5/5] Cleaner --- R/use-standalone.R | 2 +- man/use_standalone.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/use-standalone.R b/R/use-standalone.R index 9cd43c717..de796acde 100644 --- a/R/use-standalone.R +++ b/R/use-standalone.R @@ -38,7 +38,7 @@ #' field of the `DESCRIPTION` file. #' #' Note that lists are specified with standard YAML syntax, using -#' square brackets, for example: `# imports: [rlang (>= 1.0.0), purrr]`. +#' square brackets, for example: `imports: [rlang (>= 1.0.0), purrr]`. #' #' @inheritParams create_from_github #' @inheritParams use_github_file diff --git a/man/use_standalone.Rd b/man/use_standalone.Rd index 8db7ece6d..2b6fe4b2e 100644 --- a/man/use_standalone.Rd +++ b/man/use_standalone.Rd @@ -71,7 +71,7 @@ Two of these fields are consulted by `use_standalone()`: field of the `DESCRIPTION` file. Note that lists are specified with standard YAML syntax, using -square brackets, for example: `# imports: [rlang (>= 1.0.0), purrr]`. +square brackets, for example: `imports: [rlang (>= 1.0.0), purrr]`. [use_package()]: R:use_package()