From 4745a0278dbcc4aaa87c9362a4da66cf1fa6cb12 Mon Sep 17 00:00:00 2001 From: parmsam Date: Mon, 8 Jul 2024 15:10:07 -0400 Subject: [PATCH 1/7] ensure check extension approval returns true on y --- R/utils-prompt.R | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/R/utils-prompt.R b/R/utils-prompt.R index fb0c00f..7e6c935 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -1,7 +1,7 @@ check_extension_approval <- function(no_prompt = FALSE, what = "Something", see_more_at = NULL) { if (no_prompt) return(TRUE) - if (!rlang::is_interactive()) { + if (!is_interactive()) { cli::cli_abort(c( "{ what } requires explicit approval.", ">" = "Set {.arg no_prompt = TRUE} if you agree.", @@ -19,5 +19,13 @@ check_extension_approval <- function(no_prompt = FALSE, what = "Something", see_ cli::cli_inform("{what} not installed.") return(invisible(FALSE)) } + return(invisible(TRUE)) } } + +# Add binding to base R function for testthat mocking +readline <- NULL +# Add binding to function from other package for mocking later on +is_interactive <- function(...) { + rlang::is_interactive(...) +} From 7e424704c841f383ffba13444cc102c469fd7e58 Mon Sep 17 00:00:00 2001 From: parmsam Date: Mon, 8 Jul 2024 15:10:21 -0400 Subject: [PATCH 2/7] add unit tests with mocking in check extesnion approval func --- tests/testthat/test-utils-prompt.R | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/testthat/test-utils-prompt.R diff --git a/tests/testthat/test-utils-prompt.R b/tests/testthat/test-utils-prompt.R new file mode 100644 index 0000000..52eaa4e --- /dev/null +++ b/tests/testthat/test-utils-prompt.R @@ -0,0 +1,36 @@ +test_that("Checking extension with approval prompt mocked y", { + local_mocked_bindings( + readline = function(...) "y", + is_interactive = function() TRUE + ) + expect_true({ + check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) +}) + +test_that("Checking extension with approval prompt mocked n", { + local_mocked_bindings( + readline = function(...) "n", + is_interactive = function() TRUE + ) + expect_false({ + check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) +}) + +test_that("Checking extension approval", { + skip_if_no_quarto() + skip_if_offline("github.com") + + expect_true(check_extension_approval(TRUE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html")) + expect_true(check_extension_approval(TRUE, "Quarto templates", "https://quarto.org/docs/extensions/formats.html#distributing-formats")) + + expect_error({ + local_reproducible_output(rlang_interactive = FALSE) + check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) + expect_error({ + local_reproducible_output(rlang_interactive = FALSE) + check_extension_approval(TRUE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) +}) From 2c847b4c03aec7dd190867ffabff14e42fb5c817 Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 10:58:56 -0400 Subject: [PATCH 3/7] add check removal approval func --- R/utils-prompt.R | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/R/utils-prompt.R b/R/utils-prompt.R index 7e6c935..1407baf 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -23,6 +23,26 @@ check_extension_approval <- function(no_prompt = FALSE, what = "Something", see_ } } +check_removal_approval <- function(no_prompt = FALSE, what = "Something", see_more_at = NULL) { + if (no_prompt) return(TRUE) + + if (!is_interactive()) { + cli::cli_abort(c( + "{ what } requires explicit approval.", + ">" = "Set {.arg no_prompt = TRUE} if you agree.", + if (!is.null(see_more_at)) { + c(i = "See more at {.url {see_more_at}}") + } + )) + } else { + prompt_value <- tolower(readline(sprintf("? Are you sure you'd like to remove %s (Y/n)? ", what))) + if (!prompt_value %in% "y") { + return(invisible(FALSE)) + } + return(invisible(TRUE)) + } +} + # Add binding to base R function for testthat mocking readline <- NULL # Add binding to function from other package for mocking later on From f499d22e65f85bd14caca682b426a6f449ec6a20 Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 10:59:08 -0400 Subject: [PATCH 4/7] create list, remove, and update funcs --- R/list.R | 38 ++++++++++++++++++++++++++++++++++++++ R/remove.R | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ R/update.R | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 R/list.R create mode 100644 R/remove.R create mode 100644 R/update.R diff --git a/R/list.R b/R/list.R new file mode 100644 index 0000000..a555554 --- /dev/null +++ b/R/list.R @@ -0,0 +1,38 @@ +#' List Installed Quarto extensions +#' +#' List Quarto Extensions in this folder or project by running `quarto list` +#' +#' # Extension Trust +#' +#' Quarto extensions may execute code when documents are rendered. Therefore, if +#' you do not trust the author of an extension, we recommend that you do not +#' install or use the extension. +#' +#' @inheritParams quarto_render +#' +#' @examples +#' \dontrun{ +#' # List Quarto Extensions in this folder or project +#' quarto_list_extensions() +#' } +#' +#' @importFrom rlang is_interactive +#' @importFrom cli cli_abort +#' @export +quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ + quarto_bin <- find_quarto() + + args <- c("extensions", if (quiet) cli_arg_quiet(), quarto_args) + x <- quarto_list(args, quarto_bin = quarto_bin, echo = TRUE) + # Clean the stderr output to remove extra spaces and ensure consistent formatting + stderr_cleaned <- gsub("\\s+$", "", x$stderr) + if (grepl("No extensions are installed", stderr_cleaned)) { + invisible() + } else{ + invisible(read.table(text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE)) + } +} + +quarto_list <- function(args = character(), ...){ + quarto_run_what("list", args = args, ...) +} diff --git a/R/remove.R b/R/remove.R new file mode 100644 index 0000000..49f8edf --- /dev/null +++ b/R/remove.R @@ -0,0 +1,48 @@ +#' Remove a Quarto extensions +#' +#' Remove an extension in this folder or project by running `quarto remove` +#' +#' # Extension Trust +#' +#' Quarto extensions may execute code when documents are rendered. Therefore, if +#' you do not trust the author of an extension, we recommend that you do not +#' install or use the extension. +#' By default `no_prompt = FALSE` which means that +#' the function will ask for explicit approval when used interactively, or +#' disallow installation. +#' +#' @inheritParams quarto_render +#' +#' @param extension The extension to remove, either an archive or a GitHub +#' repository as described in the documentation +#' . +#' +#' @param no_prompt Do not prompt to confirm approval to download external extension. +#' +#' @examples +#' \dontrun{ +#' # Remove an already installed extension +#' quarto_remove_extension("quarto-ext/fontawesome") +#' +#' @importFrom rlang is_interactive +#' @importFrom cli cli_abort +#' @export +quarto_remove_extension <- function(extension = NULL, no_prompt = FALSE, quiet = FALSE, quarto_args = NULL) { + rlang::check_required(extension) + + quarto_bin <- find_quarto() + + # This will ask for approval or stop installation + approval <- check_removal_approval(no_prompt, extension, "https://quarto.org/docs/extensions/managing.html") + + if (approval) { + args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) + quarto_remove(args, quarto_bin = quarto_bin, echo = TRUE) + } + + invisible() +} + +quarto_remove <- function(args = character(), ...) { + quarto_run_what("remove", args = args, ...) +} diff --git a/R/update.R b/R/update.R new file mode 100644 index 0000000..4105407 --- /dev/null +++ b/R/update.R @@ -0,0 +1,52 @@ +#' Update a Quarto extensions +#' +#' Update an extension to this folder or project by running `quarto update` +#' +#' # Extension Trust +#' +#' Quarto extensions may execute code when documents are rendered. Therefore, if +#' you do not trust the author of an extension, we recommend that you do not +#' install or use the extension. +#' By default `no_prompt = FALSE` which means that +#' the function will ask for explicit approval when used interactively, or +#' disallow installation. +#' +#' @inheritParams quarto_render +#' +#' @param extension The extension to install, either an archive or a GitHub +#' repository as described in the documentation +#' . +#' +#' @param no_prompt Do not prompt to confirm approval to download external extension. +#' +#' @examples +#' \dontrun{ +#' # Update a template and set up a draft document from a GitHub repository +#' quarto_update_extension("quarto-ext/fontawesome") +#' +#' # Update a template and set up a draft document from a ZIP archive +#' quarto_update_extension("https://github.com/quarto-ext/fontawesome/archive/refs/heads/main.zip") +#' } +#' +#' @importFrom rlang is_interactive +#' @importFrom cli cli_abort +#' @export +quarto_update_extension <- function(extension = NULL, no_prompt = FALSE, quiet = FALSE, quarto_args = NULL) { + rlang::check_required(extension) + + quarto_bin <- find_quarto() + + # This will ask for approval or stop installation + approval <- check_extension_approval(no_prompt, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + + if (approval) { + args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) + quarto_update(args, quarto_bin = quarto_bin, echo = TRUE) + } + + invisible() +} + +quarto_update <- function(args = character(), ...) { + quarto_run_what("update", args = args, ...) +} From dd63903d8a1f996576ebcfcd7befa111ccb9da1f Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 10:59:15 -0400 Subject: [PATCH 5/7] add unit test for new funcs --- tests/testthat/test-list.R | 13 +++++++++++++ tests/testthat/test-remove.R | 11 +++++++++++ tests/testthat/test-update.R | 11 +++++++++++ 3 files changed, 35 insertions(+) create mode 100644 tests/testthat/test-list.R create mode 100644 tests/testthat/test-remove.R create mode 100644 tests/testthat/test-update.R diff --git a/tests/testthat/test-list.R b/tests/testthat/test-list.R new file mode 100644 index 0000000..02afda8 --- /dev/null +++ b/tests/testthat/test-list.R @@ -0,0 +1,13 @@ +test_that("Listing extensions", { + skip_if_no_quarto() + skip_if_offline("github.com") + qmd <- local_qmd_file(c("content")) + withr::local_dir(dirname(qmd)) + expect_null(quarto_list_extensions()) + quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) + expect_equal(nrow(quarto_list_extensions()), 1) + quarto_add_extension("quarto-ext/lightbox", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/lightbox")) + expect_equal(nrow(quarto_list_extensions()), 2) +}) diff --git a/tests/testthat/test-remove.R b/tests/testthat/test-remove.R new file mode 100644 index 0000000..bfe652f --- /dev/null +++ b/tests/testthat/test-remove.R @@ -0,0 +1,11 @@ +test_that("Removing an extension", { + skip_if_no_quarto() + skip_if_offline("github.com") + qmd <- local_qmd_file(c("content")) + withr::local_dir(dirname(qmd)) + expect_null(quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE)) + quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) + quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(!dir.exists("_extensions/quarto-ext/fontawesome")) +}) diff --git a/tests/testthat/test-update.R b/tests/testthat/test-update.R new file mode 100644 index 0000000..c1867a2 --- /dev/null +++ b/tests/testthat/test-update.R @@ -0,0 +1,11 @@ +test_that("Updating an extension", { + skip_if_no_quarto() + skip_if_offline("github.com") + qmd <- local_qmd_file(c("content")) + withr::local_dir(dirname(qmd)) + expect_error(quarto_add_extension("quarto-ext/fontawesome@v0.0.1"), "explicit approval") + quarto_update_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) + current_version <- yaml::read_yaml("_extensions/quarto-ext/fontawesome/_extension.yml")$version + expect_false(identical(current_version, "v0.0.1")) +}) From 33663ca1c8d9e5cd07fa5a7a40a714e0e1a3328d Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 11:03:04 -0400 Subject: [PATCH 6/7] adjust roxygen2 headers --- R/list.R | 6 ------ R/update.R | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/R/list.R b/R/list.R index a555554..50d92ae 100644 --- a/R/list.R +++ b/R/list.R @@ -2,12 +2,6 @@ #' #' List Quarto Extensions in this folder or project by running `quarto list` #' -#' # Extension Trust -#' -#' Quarto extensions may execute code when documents are rendered. Therefore, if -#' you do not trust the author of an extension, we recommend that you do not -#' install or use the extension. -#' #' @inheritParams quarto_render #' #' @examples diff --git a/R/update.R b/R/update.R index 4105407..1d6dee8 100644 --- a/R/update.R +++ b/R/update.R @@ -13,7 +13,7 @@ #' #' @inheritParams quarto_render #' -#' @param extension The extension to install, either an archive or a GitHub +#' @param extension The extension to update, either an archive or a GitHub #' repository as described in the documentation #' . #' From fa71abf33e6fcbc4cfe8f2467e3f895e058760ff Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 11:22:46 -0400 Subject: [PATCH 7/7] rebuild documentation --- NAMESPACE | 4 +++ R/list.R | 3 ++- R/remove.R | 2 +- man/quarto_list_extensions.Rd | 26 ++++++++++++++++++ man/quarto_publish_doc.Rd | 14 +++++----- man/quarto_remove_extension.Rd | 45 +++++++++++++++++++++++++++++++ man/quarto_update_extension.Rd | 49 ++++++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 man/quarto_list_extensions.Rd create mode 100644 man/quarto_remove_extension.Rd create mode 100644 man/quarto_update_extension.Rd diff --git a/NAMESPACE b/NAMESPACE index 9e9fa9f..8a7e6d5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,14 +5,17 @@ export(quarto_add_extension) export(quarto_binary_sitrep) export(quarto_create_project) export(quarto_inspect) +export(quarto_list_extensions) export(quarto_path) export(quarto_preview) export(quarto_preview_stop) export(quarto_publish_app) export(quarto_publish_doc) export(quarto_publish_site) +export(quarto_remove_extension) export(quarto_render) export(quarto_serve) +export(quarto_update_extension) export(quarto_use_template) export(quarto_version) import(rlang) @@ -28,4 +31,5 @@ importFrom(rstudioapi,isAvailable) importFrom(rstudioapi,viewer) importFrom(tools,vignetteEngine) importFrom(utils,browseURL) +importFrom(utils,read.table) importFrom(yaml,write_yaml) diff --git a/R/list.R b/R/list.R index 50d92ae..6aa76d8 100644 --- a/R/list.R +++ b/R/list.R @@ -12,6 +12,7 @@ #' #' @importFrom rlang is_interactive #' @importFrom cli cli_abort +#' @importFrom utils read.table #' @export quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ quarto_bin <- find_quarto() @@ -23,7 +24,7 @@ quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ if (grepl("No extensions are installed", stderr_cleaned)) { invisible() } else{ - invisible(read.table(text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE)) + invisible(utils::read.table(text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE)) } } diff --git a/R/remove.R b/R/remove.R index 49f8edf..cc8f54b 100644 --- a/R/remove.R +++ b/R/remove.R @@ -23,7 +23,7 @@ #' \dontrun{ #' # Remove an already installed extension #' quarto_remove_extension("quarto-ext/fontawesome") -#' +#' } #' @importFrom rlang is_interactive #' @importFrom cli cli_abort #' @export diff --git a/man/quarto_list_extensions.Rd b/man/quarto_list_extensions.Rd new file mode 100644 index 0000000..aba1743 --- /dev/null +++ b/man/quarto_list_extensions.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/list.R +\name{quarto_list_extensions} +\alias{quarto_list_extensions} +\title{List Installed Quarto extensions} +\usage{ +quarto_list_extensions(quiet = FALSE, quarto_args = NULL) +} +\arguments{ +\item{quiet}{Suppress warning and other messages.} + +\item{quarto_args}{Character vector of other \code{quarto} CLI arguments to append +to the Quarto command executed by this function. This is mainly intended for +advanced usage and useful for CLI arguments which are not yet mirrored in a +dedicated parameter of this \R function. See \verb{quarto render --help} for options.} +} +\description{ +List Quarto Extensions in this folder or project by running \verb{quarto list} +} +\examples{ +\dontrun{ +# List Quarto Extensions in this folder or project +quarto_list_extensions() +} + +} diff --git a/man/quarto_publish_doc.Rd b/man/quarto_publish_doc.Rd index df6c011..8e6ae6f 100644 --- a/man/quarto_publish_doc.Rd +++ b/man/quarto_publish_doc.Rd @@ -50,11 +50,12 @@ Defaults to the name of the \code{input}.} supplied, will often be displayed in favor of the name. When deploying a new document, you may supply only the title to receive an auto-generated name} -\item{account, server}{Uniquely identify a remote server with either your -user \code{account}, the \code{server} name, or both. If neither are supplied, and -there are multiple options, you'll be prompted to pick one. +\item{server}{Server name. Required only if you use the same account name on +multiple servers.} -Use \code{\link[rsconnect:accounts]{accounts()}} to see the full list of available options.} +\item{account}{Account to deploy application to. This parameter is only +required for the initial deployment of an application when there are +multiple accounts configured on the system (see \link[rsconnect]{accounts}).} \item{render}{\code{local} to render locally before publishing; \code{server} to render on the server; \code{none} to use whatever rendered content currently @@ -62,10 +63,7 @@ exists locally. (defaults to \code{local})} \item{metadata}{Additional metadata fields to save with the deployment record. These fields will be returned on subsequent calls to -\code{\link[rsconnect:deployments]{deployments()}}. - -Multi-value fields are recorded as comma-separated values and returned in -that form. Custom value serialization is the responsibility of the caller.} +\code{\link[rsconnect:deployments]{deployments()}}.} \item{...}{Named parameters to pass along to \code{rsconnect::deployApp()}} } diff --git a/man/quarto_remove_extension.Rd b/man/quarto_remove_extension.Rd new file mode 100644 index 0000000..e296ba1 --- /dev/null +++ b/man/quarto_remove_extension.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/remove.R +\name{quarto_remove_extension} +\alias{quarto_remove_extension} +\title{Remove a Quarto extensions} +\usage{ +quarto_remove_extension( + extension = NULL, + no_prompt = FALSE, + quiet = FALSE, + quarto_args = NULL +) +} +\arguments{ +\item{extension}{The extension to remove, either an archive or a GitHub +repository as described in the documentation +\url{https://quarto.org/docs/extensions/managing.html}.} + +\item{no_prompt}{Do not prompt to confirm approval to download external extension.} + +\item{quiet}{Suppress warning and other messages.} + +\item{quarto_args}{Character vector of other \code{quarto} CLI arguments to append +to the Quarto command executed by this function. This is mainly intended for +advanced usage and useful for CLI arguments which are not yet mirrored in a +dedicated parameter of this \R function. See \verb{quarto render --help} for options.} +} +\description{ +Remove an extension in this folder or project by running \verb{quarto remove} +} +\section{Extension Trust}{ +Quarto extensions may execute code when documents are rendered. Therefore, if +you do not trust the author of an extension, we recommend that you do not +install or use the extension. +By default \code{no_prompt = FALSE} which means that +the function will ask for explicit approval when used interactively, or +disallow installation. +} + +\examples{ +\dontrun{ +# Remove an already installed extension +quarto_remove_extension("quarto-ext/fontawesome") +} +} diff --git a/man/quarto_update_extension.Rd b/man/quarto_update_extension.Rd new file mode 100644 index 0000000..372b1eb --- /dev/null +++ b/man/quarto_update_extension.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/update.R +\name{quarto_update_extension} +\alias{quarto_update_extension} +\title{Update a Quarto extensions} +\usage{ +quarto_update_extension( + extension = NULL, + no_prompt = FALSE, + quiet = FALSE, + quarto_args = NULL +) +} +\arguments{ +\item{extension}{The extension to update, either an archive or a GitHub +repository as described in the documentation +\url{https://quarto.org/docs/extensions/managing.html}.} + +\item{no_prompt}{Do not prompt to confirm approval to download external extension.} + +\item{quiet}{Suppress warning and other messages.} + +\item{quarto_args}{Character vector of other \code{quarto} CLI arguments to append +to the Quarto command executed by this function. This is mainly intended for +advanced usage and useful for CLI arguments which are not yet mirrored in a +dedicated parameter of this \R function. See \verb{quarto render --help} for options.} +} +\description{ +Update an extension to this folder or project by running \verb{quarto update} +} +\section{Extension Trust}{ +Quarto extensions may execute code when documents are rendered. Therefore, if +you do not trust the author of an extension, we recommend that you do not +install or use the extension. +By default \code{no_prompt = FALSE} which means that +the function will ask for explicit approval when used interactively, or +disallow installation. +} + +\examples{ +\dontrun{ +# Update a template and set up a draft document from a GitHub repository +quarto_update_extension("quarto-ext/fontawesome") + +# Update a template and set up a draft document from a ZIP archive +quarto_update_extension("https://github.com/quarto-ext/fontawesome/archive/refs/heads/main.zip") +} + +}