diff --git a/DESCRIPTION b/DESCRIPTION index 913b0fb..217465b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -9,9 +9,20 @@ Description: Only exists while we manage the migration between orderly versions. License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.1 +RoxygenNote: 7.2.3 URL: https://github.com/mrc-ide/orderly.helper BugReports: https://github.com/mrc-ide/orderly.helper/issues +Imports: + rprojroot, + pkgload, + yaml Suggests: - testthat (>= 3.0.0) + mockery, + orderly1, + orderly2, + testthat (>= 3.0.0), + withr Config/testthat/edition: 3 +Remotes: + orderly1=vimc/orderly@vimc-7135, + mrc-ide/orderly2 diff --git a/NAMESPACE b/NAMESPACE index e651b94..f5ddedc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1 +1,7 @@ # Generated by roxygen2: do not edit by hand + +export(activate) +export(auto) +export(deactivate) +export(sitrep) +export(use) diff --git a/R/helper.R b/R/helper.R new file mode 100644 index 0000000..da6ed6d --- /dev/null +++ b/R/helper.R @@ -0,0 +1,247 @@ +##' Activate orderly for the current repository. Run this from +##' anywhere within an orderly archive (i.e., a path containing +##' orderly_config.yml; or where one of its parent directories do). We +##' check what the required orderly minimum version is and set up the +##' orderly namespace for the appropriate version. +##' +##' @title Activate orderly for current reposotory +##' +##' @param verbose Be verbose about what we are doing. If `NULL` uses +##' the the value of the option `orderly.helper.verbose` +##' +##' @return Nothing, called for side effects only. +##' @export +activate <- function(verbose = NULL) { + version <- detect_orderly_version(getwd()) + create_orderly_ns(version, verbose) +} + + +##' Set up orderly based on your global preferences. This will look at +##' the R option (`orderly.version`) and the environment variable +##' `ORDERLY_VERSION`, in that order, falling back on the second +##' version. Or pass in a version explicitly. +##' +##' @title Set orderly version +##' +##' @param version Either `NULL` (in which case we use global +##' preferences) or a number `1` or `2`. +##' +##' @inheritParams activate +##' +##' @export +##' @return Nothing, called for side effects only. +use <- function(version = NULL, verbose = NULL) { + version <- guess_orderly_version(version) + create_orderly_ns(version, verbose) +} + + +##' Deactivate any orderly helper +##' +##' @title Deactivate orderly helper +##' @return Nothing, called for its side effect +##' @export +deactivate <- function() { + tryCatch(pkgload::unload("orderly"), error = function(e) NULL) + current$name <- NULL + current$version <- NULL + invisible() +} + + +##' Return information about the state of orderly1, orderly2 and the helper +##' +##' @title Return information about packages +##' @return A list +##' @export +sitrep <- function() { + loaded <- loadedNamespaces() + attached <- sub("^package:", "", search()) + + f <- function(p) { + version <- tryCatch(utils::packageVersion(p), error = function(e) NULL) + list(version = version, + is_installed = !is.null(version), + is_loaded = p %in% loaded, + is_attached = p %in% attached) + } + + pkg <- c("orderly", "orderly1", "orderly2") + ret <- lapply(pkg, f) + names(ret) <- pkg + + if (ret$orderly$is_installed) { + is_installed <- any( + file.exists(file.path(.libPaths(), "orderly", "DESCRIPTION"))) + if (!is_installed) { + ret$orderly <- list(version = NULL, + is_installed = FALSE, + is_loaded = FALSE, + is_attached = FALSE) + } + } + + ret$current <- list(version = current$version, name = current$name) + + ret +} + + +##' Automatically set up the orderly namespace depending on the +##' context in which this function is called. +##' +##' If called from within an orderly repository, it will activate the +##' required version using [activate()] otherwise it will use the +##' globally preferred version with [use()] +##' +##' @title Automatically configure orderly +##' @return Nothing, called for side effects only +##' @export +auto <- function() { + tryCatch(activate(), error = function(e) use()) +} + + +## Persistant package state goes here +current <- new.env(parent = emptyenv()) + +create_orderly_ns <- function(version, verbose) { + check_sitrep() + name <- sprintf("orderly%d", version) + verbose <- orderly_helper_verbose(verbose) + if (identical(version, current$version)) { + if (verbose) { + message(sprintf("Already using %s", orderly_version_str(version))) + } + return(invisible()) + } + if (verbose) { + message(sprintf("Using %s", orderly_version_str(version))) + } + path <- find.package(name) + + desc_contents <- readLines(file.path(path, "DESCRIPTION")) + i <- grep("^Package:", desc_contents) + desc_contents[[i]] <- "Package: orderly" + + exports <- getNamespaceExports(asNamespace(name)) + ns_contents <- c(sprintf('import("%s")', name), + sprintf('export("%s")', exports)) + + tmp <- temp_package_dir(name) + writeLines(desc_contents, file.path(tmp, "DESCRIPTION")) + writeLines(ns_contents, file.path(tmp, "NAMESPACE")) + res <- pkgload::load_all(tmp, attach = FALSE, quiet = !verbose) + + ## Lastly, we might wire up the help too: + ## + ## pkgload:::dev_help(topic_str, package_str) -> + ## utils::help(topic_str, "orderly1") + ## + ## also system.file and vignette need dealing with; these might be + ## somewhat trickier though, and devtools/pkgload don't try and pull + ## it off, so we can't rely on assistance there. + ## + ## Also don't support ':::' access; that's reasonable though. + + current$name <- name + current$version <- version + + invisible() +} + + +check_sitrep <- function(info = sitrep()) { + if (info$orderly$is_installed) { + stop(paste("You have 'orderly' installed; please uninstall it first and", + "install 'orderly1' and/or 'orderly2' instead")) + } + if (info$orderly1$is_attached && info$orderly2$is_attached) { + stop(paste("You have 'orderly1' and 'orderly2' attached; please", + "restart your session")) + } + invisible(info) +} + + +temp_package_dir <- function(name) { + if (is.null(current$path)) { + current$path <- tempfile() + } + path <- file.path(current$path, name) + dir.create(path, FALSE, TRUE) + path +} + + +orderly_helper_verbose <- function(verbose) { + verbose %||% getOption("orderly.helper.verbose", TRUE) +} + + +detect_orderly_version <- function(path) { + root <- find_orderly_root(path) + d <- yaml::yaml.load_file(file.path(root, "orderly_config.yml")) + version_str <- d$minimum_orderly_version + if (is.null(version_str)) { + stop(sprintf("Failed to read required orderly version from '%s'", path)) + } + version <- numeric_version(version_str) + if (version < numeric_version("1.99.0")) 1 else 2 +} + + +guess_orderly_version <- function(version) { + if (!is.null(version)) { + return(validate_orderly_version(version, "argument 'version'", FALSE)) + } + + version <- getOption("orderly.version", NULL) + if (!is.null(version)) { + return(validate_orderly_version(version, "option 'orderly.version'")) + } + version <- Sys.getenv("ORDERLY_VERSION", NA_character_) + if (!is.na(version)) { + return(validate_orderly_version(version, + "environment variable 'ORDERLY_VERSION'", + TRUE)) + } + 2 +} + + +validate_orderly_version <- function(value, name, from_character = FALSE) { + if (from_character) { + if (!grepl("^[0-9]$", value)) { + stop(sprintf("Expected 'version' to be a number (from %s)", name)) + } + value <- as.numeric(value) + } else { + if (!(is.numeric(value) && length(value) == 1 && !is.na(value))) { + stop(sprintf("Expected 'version' to be scalar number (from %s)", name)) + } + } + if (!(value %in% 1:2)) { + stop(sprintf("Invalid version '%s', expected '1' or '2' (from %s)", + value, name)) + } + value +} + + +orderly_version_str <- function(major) { + name <- sprintf("orderly%d", major) + version <- tryCatch(utils::packageVersion(name), error = function(e) "???") + sprintf("orderly %d (%s)", major, as.character(version)) +} + + +find_orderly_root <- function(start) { + tryCatch( + rprojroot::find_root(rprojroot::has_file("orderly_config.yml"), + path = start), + error = function(e) { + stop(sprintf("Did not find orderly root above '%s'", start)) + }) +} diff --git a/README.md b/README.md index 7ebc55d..4545bb7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,47 @@ [![codecov.io](https://codecov.io/github/mrc-ide/orderly.helper/coverage.svg?branch=main)](https://codecov.io/github/mrc-ide/orderly.helper?branch=main) +This package exists to smooth over the difference between [`orderly1`](https://vaccineimpact.org/orderly) and [`orderly2`](https://mrc-ide.github.io/orderly2) while we manage the migration between the two packages. It will allow you to refer to either (but at the same time only *one*) of the packages by its namespace, so that + +``` +orderly::orderly_run(...) +``` + +will run `orderly_run` in one of the packages. + +Within an orderly repo, you can then run: + +``` +orderly.helper::activate() +``` + +which will configure everything for you. + +Alternatively, run + +```r +orderly.helper::use() +``` + +which will set up orderly based on your personal preferences. + +If you don't want to think about any of this, you can call + +```r +orderly.helper::auto() +``` + +We recommend adding to your `.Rprofile`: + +```{r} +options(orderly.version = 2, orderly.helper.verbose = TRUE) +if (!require("orderly.helper", quietly = TRUE)) { + orderly.helper::auto() +} +``` + +With the two options configured as you prefer. + ## Installation To install `orderly.helper`: diff --git a/man/activate.Rd b/man/activate.Rd new file mode 100644 index 0000000..13e1638 --- /dev/null +++ b/man/activate.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helper.R +\name{activate} +\alias{activate} +\title{Activate orderly for current reposotory} +\usage{ +activate(verbose = NULL) +} +\arguments{ +\item{verbose}{Be verbose about what we are doing. If \code{NULL} uses +the the value of the option \code{orderly.helper.verbose}} +} +\value{ +Nothing, called for side effects only. +} +\description{ +Activate orderly for the current repository. Run this from +anywhere within an orderly archive (i.e., a path containing +orderly_config.yml; or where one of its parent directories do). We +check what the required orderly minimum version is and set up the +orderly namespace for the appropriate version. +} diff --git a/man/auto.Rd b/man/auto.Rd new file mode 100644 index 0000000..8c70b34 --- /dev/null +++ b/man/auto.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helper.R +\name{auto} +\alias{auto} +\title{Automatically configure orderly} +\usage{ +auto() +} +\value{ +Nothing, called for side effects only +} +\description{ +Automatically set up the orderly namespace depending on the +context in which this function is called. +} +\details{ +If called from within an orderly repository, it will activate the +required version using \code{\link[=activate]{activate()}} otherwise it will use the +globally preferred version with \code{\link[=use]{use()}} +} diff --git a/man/deactivate.Rd b/man/deactivate.Rd new file mode 100644 index 0000000..b154a7c --- /dev/null +++ b/man/deactivate.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helper.R +\name{deactivate} +\alias{deactivate} +\title{Deactivate orderly helper} +\usage{ +deactivate() +} +\value{ +Nothing, called for its side effect +} +\description{ +Deactivate any orderly helper +} diff --git a/man/sitrep.Rd b/man/sitrep.Rd new file mode 100644 index 0000000..dc2effb --- /dev/null +++ b/man/sitrep.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helper.R +\name{sitrep} +\alias{sitrep} +\title{Return information about packages} +\usage{ +sitrep() +} +\value{ +A list +} +\description{ +Return information about the state of orderly1, orderly2 and the helper +} diff --git a/man/use.Rd b/man/use.Rd new file mode 100644 index 0000000..842b52a --- /dev/null +++ b/man/use.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helper.R +\name{use} +\alias{use} +\title{Set orderly version} +\usage{ +use(version = NULL, verbose = NULL) +} +\arguments{ +\item{version}{Either \code{NULL} (in which case we use global +preferences) or a number \code{1} or \code{2}.} + +\item{verbose}{Be verbose about what we are doing. If \code{NULL} uses +the the value of the option \code{orderly.helper.verbose}} +} +\value{ +Nothing, called for side effects only. +} +\description{ +Set up orderly based on your global preferences. This will look at +the R option (\code{orderly.version}) and the environment variable +\code{ORDERLY_VERSION}, in that order, falling back on the second +version. Or pass in a version explicitly. +} diff --git a/tests/testthat/test-helper.R b/tests/testthat/test-helper.R new file mode 100644 index 0000000..b6d52c5 --- /dev/null +++ b/tests/testthat/test-helper.R @@ -0,0 +1,217 @@ +test_that("orderly_version_str constructs good strings", { + skip_if_not_installed("mockery") + mock_package_version <- mockery::mock( + numeric_version("1.7.0"), + numeric_version("1.99.0"), + stop("package not installed")) + mockery::stub(orderly_version_str, "utils::packageVersion", + mock_package_version) + expect_equal( + orderly_version_str(1), "orderly 1 (1.7.0)") + expect_equal( + orderly_version_str(2), "orderly 2 (1.99.0)") + expect_equal( + orderly_version_str(2), "orderly 2 (???)") +}) + + +test_that("can validate orderly version", { + expect_equal(validate_orderly_version(1, "arg", FALSE), 1) + expect_equal(validate_orderly_version(2, "arg", FALSE), 2) + expect_equal(validate_orderly_version("1", "arg", TRUE), 1) + expect_equal(validate_orderly_version("2", "arg", TRUE), 2) + + expect_error(validate_orderly_version(1:2, "arg", FALSE), + "Expected 'version' to be scalar number (from arg)", + fixed = TRUE) + expect_error(validate_orderly_version("1", "arg", FALSE), + "Expected 'version' to be scalar number (from arg)", + fixed = TRUE) + expect_error(validate_orderly_version(NA_real_, "arg", FALSE), + "Expected 'version' to be scalar number (from arg)", + fixed = TRUE) + expect_error(validate_orderly_version(3, "arg", FALSE), + "Invalid version '3', expected '1' or '2' (from arg)", + fixed = TRUE) + + expect_error(validate_orderly_version("one", "arg", TRUE), + "Expected 'version' to be a number (from arg)", + fixed = TRUE) +}) + + +test_that("can guess orderly version", { + withr::local_envvar(ORDERLY_VERSION = NA) + withr::local_options(orderly.version = NULL) + expect_equal(guess_orderly_version(NULL), 2) + + expect_equal(guess_orderly_version(1), 1) + expect_equal(guess_orderly_version(2), 2) + + withr::with_options(list(orderly.version = 1), + expect_equal(guess_orderly_version(NULL), 1)) + withr::with_options(list(orderly.version = 2), + expect_equal(guess_orderly_version(NULL), 2)) + + withr::with_envvar(c(ORDERLY_VERSION = 1), { + expect_equal(guess_orderly_version(NULL), 1) + withr::with_options(list(orderly.version = 1), + expect_equal(guess_orderly_version(NULL), 1)) + withr::with_options(list(orderly.version = 2), + expect_equal(guess_orderly_version(NULL), 2)) + }) +}) + + +test_that("can respond to verbose option", { + withr::local_options(orderly.helper.verbose = NULL) + expect_true(orderly_helper_verbose(NULL)) + expect_true(orderly_helper_verbose(TRUE)) + expect_false(orderly_helper_verbose(FALSE)) + withr::with_options(list(orderly.helper.verbose = FALSE), { + expect_false(orderly_helper_verbose(NULL)) + expect_true(orderly_helper_verbose(TRUE)) + expect_false(orderly_helper_verbose(FALSE)) + }) +}) + + +test_that("can detect orderly version", { + tmp <- withr::local_tempdir() + expect_error( + detect_orderly_version(tmp), + "Did not find orderly root above") + file.create(file.path(tmp, "orderly_config.yml")) + expect_error( + detect_orderly_version(tmp), + "Failed to read required orderly version from") + writeLines("minimum_orderly_version: 1.7.0", + file.path(file.path(tmp, "orderly_config.yml"))) + expect_equal(detect_orderly_version(tmp), 1) + writeLines("minimum_orderly_version: 1.99.0", + file.path(file.path(tmp, "orderly_config.yml"))) + expect_equal(detect_orderly_version(tmp), 2) + writeLines("minimum_orderly_version: 2.0.0", + file.path(file.path(tmp, "orderly_config.yml"))) + expect_equal(detect_orderly_version(tmp), 2) +}) + + +test_that("can set up orderly1 as orderly", { + current$name <- NULL + msg <- testthat::capture_messages(create_orderly_ns(1, TRUE)) + expect_match(msg, "Using orderly 1 \\(1.\\d+.\\d+\\)", all = FALSE) + expect_identical(getExportedValue("orderly", "orderly_run"), + orderly1::orderly_run) + + msg <- testthat::capture_messages(create_orderly_ns(1, TRUE)) + expect_match(msg, "Already using orderly 1 \\(1.\\d+.\\d+\\)", all = FALSE) + expect_identical(getExportedValue("orderly", "orderly_run"), + orderly1::orderly_run) +}) + + +test_that("can set up orderly2 as orderly", { + expect_silent(create_orderly_ns(2, FALSE)) + expect_identical(getExportedValue("orderly", "orderly_run"), + orderly2::orderly_run) + + expect_silent(create_orderly_ns(2, FALSE)) + expect_identical(getExportedValue("orderly", "orderly_run"), + orderly2::orderly_run) +}) + + +test_that("sitrep returns useful information", { + deactivate() + ans <- sitrep() + expect_equal(ans$orderly, + list(version = NULL, + is_installed = FALSE, + is_loaded = FALSE, + is_attached = FALSE)) + expect_equal(ans$orderly1, + list(version = packageVersion("orderly1"), + is_installed = TRUE, + is_loaded = TRUE, + is_attached = FALSE)) + expect_equal(ans$orderly2, + list(version = packageVersion("orderly2"), + is_installed = TRUE, + is_loaded = TRUE, + is_attached = FALSE)) + expect_equal(ans$current, list(version = NULL, name = NULL)) +}) + + +test_that("sitrep returns useful information after helper", { + deactivate() + ans1 <- sitrep() + create_orderly_ns(2, FALSE) + ans2 <- sitrep() + expect_equal(ans2[names(ans2) != "current"], ans1[names(ans1) != "current"]) + expect_equal(ans2$current, list(version = 2, name = "orderly2")) +}) + + +test_that("sitrep check throws when expected", { + expect_silent( + check_sitrep( + list(orderly = list(is_installed = FALSE), + orderly1 = list(is_attached = FALSE), + orderly2 = list(is_attached = FALSE)))) + expect_silent( + check_sitrep( + list(orderly = list(is_installed = FALSE), + orderly1 = list(is_attached = TRUE), + orderly2 = list(is_attached = FALSE)))) + expect_silent( + check_sitrep( + list(orderly = list(is_installed = FALSE), + orderly1 = list(is_attached = FALSE), + orderly2 = list(is_attached = TRUE)))) + expect_error( + check_sitrep( + list(orderly = list(is_installed = TRUE), + orderly1 = list(is_attached = FALSE), + orderly2 = list(is_attached = FALSE))), + "You have 'orderly' installed; please uninstall it") + expect_error( + check_sitrep( + list(orderly = list(is_installed = FALSE), + orderly1 = list(is_attached = TRUE), + orderly2 = list(is_attached = TRUE))), + "You have 'orderly1' and 'orderly2' attached; please restart") +}) + + +test_that("use works with given version", { + deactivate() + use(1, FALSE) + expect_equal(current$version, 1) +}) + + +test_that("activate works with found version", { + deactivate() + tmp <- withr::local_tempdir() + writeLines("minimum_orderly_version: 1.7.0", + file.path(file.path(tmp, "orderly_config.yml"))) + withr::with_dir(tmp, activate(FALSE)) + expect_equal(current$version, 1) +}) + + +test_that("auto wrapper does the right thing", { + skip_if_not_installed("mockery") + mock_activate <- mockery::mock(NULL, stop("failure")) + mock_use <- mockery::mock() + mockery::stub(auto, "activate", mock_activate) + mockery::stub(auto, "use", mock_use) + auto() + mockery::expect_called(mock_activate, 1) + mockery::expect_called(mock_use, 0) + auto() + mockery::expect_called(mock_activate, 2) + mockery::expect_called(mock_use, 1) +})