diff --git a/NEWS.md b/NEWS.md index 46f6ce9b..bb110ade 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # btw (development version) +## New features + +* `btw docs` and `btw pkg src` commands now auto-load an in-development package found in the current directory (or its `pkg-r/`, `R/` subfolder) with `pkgload::load_all()` before running, so results reflect uncommitted local changes. Pass `--no-dev` to disable this and use the installed package instead (#212). + # btw 1.4.0 ## New features diff --git a/exec/btw.R b/exec/btw.R index e75dbe1d..d0954b02 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -27,6 +27,51 @@ if (version) { has_value <- function(x) !is.na(x) && nzchar(x) +btw_dev_package_candidate_dirs <- c(".", "pkg-r", "R") + +btw_find_dev_package_dir <- function(package) { + for (dir in btw_dev_package_candidate_dirs) { + desc_path <- file.path(dir, "DESCRIPTION") + if (!file.exists(desc_path)) { + next + } + found <- tryCatch( + unname(read.dcf(desc_path, fields = "Package")[1, "Package"]), + error = function(e) NA_character_ + ) + if (!is.na(found) && identical(found, package)) { + return(dir) + } + } + NULL +} + +btw_maybe_load_dev_package <- function(package, no_dev = FALSE) { + if (no_dev || !has_value(package)) { + return(invisible(NULL)) + } + if (!requireNamespace("pkgload", quietly = TRUE)) { + return(invisible(NULL)) + } + + dev_dir <- btw_find_dev_package_dir(package) + if (is.null(dev_dir)) { + return(invisible(NULL)) + } + + dev_dir_display <- if (identical(dev_dir, ".")) { + "" + } else { + cli::format_inline(" from {.path {dev_dir}}") + } + + cli::cli_progress_step( + "Loaded in-development package {.pkg {package}}{dev_dir_display} (pass {.field --no-dev} to disable)." + ) + pkgload::load_all(dev_dir, quiet = TRUE) + invisible(NULL) +} + btw_json_output <- function(x) { cat( jsonlite::toJSON( @@ -66,7 +111,7 @@ btw_self_help <- function(...) { # Command implementations ----------------------------------------------------- -btw_docs_help <- function(topic, package) { +btw_docs_help <- function(topic, package, no_dev = FALSE) { if (grepl("::", topic, fixed = TRUE)) { parts <- strsplit(topic, "::", fixed = TRUE)[[1]] if (has_value(package)) { @@ -77,8 +122,10 @@ btw_docs_help <- function(topic, package) { call. = FALSE ) } + btw_maybe_load_dev_package(parts[1], no_dev) btw_output(btw_this(btw:::as_btw_docs_topic(parts[1], parts[2]))) } else if (has_value(package)) { + btw_maybe_load_dev_package(package, no_dev) btw_output(btw_this(btw:::as_btw_docs_topic(package, topic))) } else { result <- tryCatch( @@ -86,6 +133,7 @@ btw_docs_help <- function(topic, package) { error = function(e) NULL ) if (is.null(result)) { + btw_maybe_load_dev_package(topic, no_dev) btw_output(btw_this(btw:::as_btw_docs_package(topic))) } else { btw_output(result) @@ -93,7 +141,8 @@ btw_docs_help <- function(topic, package) { } } -btw_docs_topics <- function(package, only, json = FALSE) { +btw_docs_topics <- function(package, only, json = FALSE, no_dev = FALSE) { + btw_maybe_load_dev_package(package, no_dev) if (!only %in% c("", "help", "vignettes")) { stop("--only must be \"help\" or \"vignettes\"", call. = FALSE) } @@ -182,7 +231,8 @@ btw_docs_topics <- function(package, only, json = FALSE) { } } -btw_docs_vignette <- function(package, name, list) { +btw_docs_vignette <- function(package, name, list, no_dev = FALSE) { + btw_maybe_load_dev_package(package, no_dev) if (list) { btw_output(btw_this(utils::vignette(package = package))) } else if (has_value(name)) { @@ -207,7 +257,8 @@ btw_docs_vignette <- function(package, name, list) { } } -btw_docs_news <- function(package, search) { +btw_docs_news <- function(package, search, no_dev = FALSE) { + btw_maybe_load_dev_package(package, no_dev) search_term <- if (has_value(search)) search else "" btw_output(btw:::btw_tool_docs_package_news_impl(package, search_term)) } @@ -319,14 +370,18 @@ btw_pkg_desc <- function(packages, fields = "", json = FALSE) { return(invisible(NULL)) } - output <- Map(function(description, path) { - if (include_all) { - return(paste(readLines(path, warn = FALSE), collapse = "\n")) - } + output <- Map( + function(description, path) { + if (include_all) { + return(paste(readLines(path, warn = FALSE), collapse = "\n")) + } - description$del(setdiff(description$fields(), select_fields(description))) - description$str(normalize = FALSE, mode = "file") - }, descriptions, description_paths) + description$del(setdiff(description$fields(), select_fields(description))) + description$str(normalize = FALSE, mode = "file") + }, + descriptions, + description_paths + ) cat(paste(output, collapse = "\n\n---\n\n"), "\n", sep = "") } @@ -343,7 +398,13 @@ btw_pkg_coverage <- function(path, file, json = FALSE) { } } -btw_pkg_src_list <- function(package, all = FALSE, json = FALSE) { +btw_pkg_src_list <- function( + package, + all = FALSE, + json = FALSE, + no_dev = FALSE +) { + btw_maybe_load_dev_package(package, no_dev) result <- btw:::btw_tool_pkg_src_list_impl(package, all = all) if (json) { data <- S7::prop(result, "extra")$data @@ -363,7 +424,8 @@ btw_pkg_src_path <- function(packages, json = FALSE) { } } -btw_pkg_src_get <- function(package, objects, json = FALSE) { +btw_pkg_src_get <- function(package, objects, json = FALSE, no_dev = FALSE) { + btw_maybe_load_dev_package(package, no_dev) result <- btw:::btw_tool_pkg_src_get_impl(package, objects) if (json) { data <- S7::prop(result, "extra")$data @@ -377,8 +439,10 @@ btw_pkg_src_methods <- function( package, generics, source = FALSE, - json = FALSE + json = FALSE, + no_dev = FALSE ) { + btw_maybe_load_dev_package(package, no_dev) result <- btw:::btw_tool_pkg_src_methods_impl( package, generics, @@ -392,7 +456,8 @@ btw_pkg_src_methods <- function( } } -btw_pkg_src_search <- function(package, terms, json = FALSE) { +btw_pkg_src_search <- function(package, terms, json = FALSE, no_dev = FALSE) { + btw_maybe_load_dev_package(package, no_dev) result <- btw:::btw_tool_pkg_src_search_impl(package, terms) if (json) { data <- S7::prop(result, "extra")$data @@ -756,6 +821,9 @@ switch( #| title: Access R documentation docs = { + #| description: Don't automatically load an in-development package found in the current directory (or its pkg-r/, r/, R/ subfolder). + no_dev <- FALSE + switch( docs_cmd <- "", @@ -769,7 +837,10 @@ switch( #| description: Output as JSON with top-level keys "help" (array of {topic_id, title, aliases[]}) and "vignettes" (array of {vignette, title}). json <- FALSE - tryCatch(btw_docs_topics(package, only, json), error = btw_error) + tryCatch( + btw_docs_topics(package, only, json, no_dev), + error = btw_error + ) }, #| title: Show help for a topic or package @@ -780,7 +851,7 @@ switch( #| short: 'p' package <- "" - tryCatch(btw_docs_help(topic, package), error = btw_error) + tryCatch(btw_docs_help(topic, package, no_dev), error = btw_error) }, #| title: Read a package vignette @@ -794,7 +865,10 @@ switch( #| short: 'l' list <- FALSE - tryCatch(btw_docs_vignette(package, name, list), error = btw_error) + tryCatch( + btw_docs_vignette(package, name, list, no_dev), + error = btw_error + ) }, #| title: Show package NEWS @@ -805,7 +879,7 @@ switch( #| short: 's' search <- "" - tryCatch(btw_docs_news(package, search), error = btw_error) + tryCatch(btw_docs_news(package, search, no_dev), error = btw_error) } ) if (docs_cmd == "") btw_self_help("docs") @@ -866,6 +940,9 @@ switch( #| title: Inspect package source code src = { + #| description: Don't automatically load an in-development package found in the current directory (or its pkg-r/, r/, R/ subfolder). + no_dev <- FALSE + switch( src_cmd <- "", @@ -879,7 +956,10 @@ switch( all <- FALSE #| description: Output as JSON. json <- FALSE - tryCatch(btw_pkg_src_list(package, all, json), error = btw_error) + tryCatch( + btw_pkg_src_list(package, all, json, no_dev), + error = btw_error + ) }, #| title: Show install paths for packages @@ -906,7 +986,7 @@ switch( #| description: Output as JSON. json <- FALSE tryCatch( - btw_pkg_src_get(package, `objects...`, json), + btw_pkg_src_get(package, `objects...`, json, no_dev), error = btw_error ) }, @@ -925,7 +1005,7 @@ switch( #| description: Output as JSON. json <- FALSE tryCatch( - btw_pkg_src_methods(package, `generics...`, source, json), + btw_pkg_src_methods(package, `generics...`, source, json, no_dev), error = btw_error ) }, @@ -941,7 +1021,7 @@ switch( #| description: Output as JSON. json <- FALSE tryCatch( - btw_pkg_src_search(package, `terms...`, json), + btw_pkg_src_search(package, `terms...`, json, no_dev), error = btw_error ) } diff --git a/tests/testthat/test-cli.R b/tests/testthat/test-cli.R index ecac59ba..7f3bf74a 100644 --- a/tests/testthat/test-cli.R +++ b/tests/testthat/test-cli.R @@ -1,11 +1,19 @@ use_latest_pandoc() -btw_cli_path <- function() { +# Resolved once, up front, so tests that change the working directory +# (e.g. to exercise cwd-based dev package detection) don't break path +# resolution for subsequent `test_path()` calls. +btw_cli_path_resolved <- { path <- test_path("..", "..", "exec", "btw.R") - if (!file.exists(path)) { + if (file.exists(path)) normalizePath(path) else NA_character_ +} +btw_pkg_dir_resolved <- normalizePath(test_path("..", "..")) + +btw_cli_path <- function() { + if (is.na(btw_cli_path_resolved)) { skip("btw CLI script not found") } - normalizePath(path) + btw_cli_path_resolved } run_btw <- function(...) { @@ -20,7 +28,7 @@ run_btw_quietly <- function(...) { run_btw_subprocess <- function(...) { skip_if_not_installed("processx") - pkg_dir <- normalizePath(test_path("..", "..")) + pkg_dir <- btw_pkg_dir_resolved app <- btw_cli_path() args <- c(...) quoted_args <- paste0('"', args, '"', collapse = ", ") @@ -423,6 +431,94 @@ test_that("btw pkg src methods --source --json returns source data", { expect_true(nzchar(row$source)) }) +# dev package auto-detection --------------------------------------------- + +local_dev_package <- function(name, subdir = ".", .local_envir = parent.frame()) { + root <- withr::local_tempdir(.local_envir = .local_envir) + pkg_dir <- file.path(root, subdir) + dir.create(file.path(pkg_dir, "R"), recursive = TRUE) + writeLines( + c( + paste0("Package: ", name), + "Version: 0.0.0.1", + "Title: Test", + "Description: Test.", + "License: MIT", + "Encoding: UTF-8" + ), + file.path(pkg_dir, "DESCRIPTION") + ) + writeLines( + c("#' @export", paste0(name, '_hello <- function() "hello"')), + file.path(pkg_dir, "R", "hello.R") + ) + writeLines( + c( + "# Generated by roxygen2: do not edit by hand", + "", + paste0("export(", name, "_hello)") + ), + file.path(pkg_dir, "NAMESPACE") + ) + withr::local_dir(root, .local_envir = .local_envir) + withr::defer( + if (name %in% loadedNamespaces()) { + try(pkgload::unload(name, quiet = TRUE), silent = TRUE) + }, + envir = .local_envir + ) + invisible(root) +} + +test_that("btw pkg src list auto-loads a dev package found in cwd", { + skip_if_not_installed("pkgload") + app <- btw_cli_path() + local_dev_package("devpkgone") + + expect_message( + output <- capture.output( + env <- Rapp::run(app, c("pkg", "src", "list", "devpkgone")) + ), + "Loaded in-development package" + ) + expect_match(paste(output, collapse = "\n"), "devpkgone_hello") +}) + +test_that("btw pkg src list --no-dev skips auto-loading", { + local_dev_package("devpkgtwo") + + result <- run_btw_subprocess("pkg", "src", "list", "devpkgtwo", "--no-dev") + expect_equal(result$status, 1) + expect_match(result$stderr, "devpkgtwo", ignore.case = TRUE) +}) + +test_that("btw pkg src get finds a dev package in an R/ subfolder", { + skip_if_not_installed("pkgload") + app <- btw_cli_path() + local_dev_package("devpkgthree", subdir = "R") + + expect_message( + output <- capture.output( + env <- Rapp::run( + app, + c("pkg", "src", "get", "devpkgthree", "devpkgthree_hello") + ) + ), + "Loaded in-development package" + ) + expect_match(paste(output, collapse = "\n"), "devpkgthree_hello") +}) + +test_that("btw pkg src list --no-dev flag defaults to FALSE", { + env <- run_btw_quietly("pkg", "src", "list", "stats") + expect_false(env$no_dev) +}) + +test_that("btw docs topics --no-dev flag defaults to FALSE", { + env <- run_btw_quietly("docs", "topics", "stats") + expect_false(env$no_dev) +}) + # btw info deprecated ---------------------------------------------------- test_that("btw info exits 1 with deprecation message", {