diff --git a/DESCRIPTION b/DESCRIPTION index f97fe60..cac5fbc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,14 +14,18 @@ BugReports: https://github.com/pharmaR/val.report/issues Depends: R (>= 4.3.0) Imports: + grDevices, methods, htmltools, quarto (>= 1.4.4), - reactable + reactable, + utils, + yaml Suggests: knitr, riskmetric (>= 0.2.4), rmarkdown, + S7, spelling, testthat (>= 3.0.0), withr (>= 3.0.2) diff --git a/NAMESPACE b/NAMESPACE index 0e24e4a..4384d48 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,10 +4,22 @@ export(assessment) export(check_reporter) export(environ_report) export(is_risk_error) +export(knit_print.knitr_log) +export(knitr_logger) +export(knitr_mutable_header) +export(knitr_update_options) export(options_report) export(package_report) +export(session) export(summary_table) +importFrom(grDevices,grSoftVersion) importFrom(htmltools,div) importFrom(methods,is) importFrom(tools,check_packages_in_dir_details) +importFrom(utils,capture.output) importFrom(utils,getFromNamespace) +importFrom(utils,head) +importFrom(utils,packageName) +importFrom(utils,sessionInfo) +importFrom(utils,tail) +importFrom(yaml,yaml.load) diff --git a/R/render_utils.R b/R/render_utils.R index 2eaf721..639927f 100644 --- a/R/render_utils.R +++ b/R/render_utils.R @@ -12,7 +12,7 @@ extract_license <- function(assessment) { if (getRversion() < "4.4.0") { analyze_license <- utils::getFromNamespace("tools", "analyze_license") # nolint } else { - analyze_license <- tools::analyze_license + analyze_license <- getExportedValue("tools", "analyze_license") } license_analysis <- analyze_license(license_output$main) diff --git a/R/reporter.R b/R/reporter.R index 42eccab..c3efc85 100644 --- a/R/reporter.R +++ b/R/reporter.R @@ -2,139 +2,169 @@ #' #' @param package_name Package name. #' @param package_version Package version number. -#' @param package Path where to find a package source to retrieve name and version number. -#' @param template_path Path to a directory with one quarto template file (and the files required for rendering it). +#' @param package Path where to find a package source to retrieve name and +#' version number. +#' @param template_path Path to a directory with one quarto template file (and +#' the files required for rendering it). #' @param output_format Output format for the report. Default is "all". #' @param params A list of execute parameters passed to the template #' @param ... Additional arguments passed to `quarto::quarto_render()` #' #' @return A path to the reports generated, called by its side effects. -#' @details Please include source as part of `params` content. Source is returned after -#' calling function `riskmetric::pkg_ref` before the risk assessment is executed -#' @export +#' +#' @details Please include source as part of `params` content. Source is +#' returned after calling function `riskmetric::pkg_ref` before the risk +#' assessment is executed +#' #' @examples #' options("valreport_output_dir" = tempdir()) #' pr <- package_report( #' package_name = "dplyr", #' package_version = "1.1.4", #' params = list( -#' assessment_path = system.file("assessments/dplyr.rds", package = "val.report"), +#' assessment_path = +#' system.file("assessments/dplyr.rds", package = "val.report"), #' image = "rhub/ref-image"), #' quiet = FALSE #' ) #' pr #' file.remove(pr) +#' +#' @export package_report <- function( - package_name, - package_version, - package = NULL, - template_path = system.file("report/package", package = "val.report"), - output_format = "all", - params = list(), - ... + package_name, + package_version, + package = NULL, + template_path = system.file("report/package", package = "val.report"), + output_format = "all", + params = list(), + ... ) { - empty_pkg_info <- is.empty(package_name) && is.empty(package_version) - if (empty_pkg_info && !is.empty(package)) { - package_name <- basename(package) - desc <- read.dcf(file.path(package, "DESCRIPTION")) - - stopifnot("Mismatch between path and DESCRIPTION name" = package_name == desc[, "Package"]) - package_version <- desc[, "Version"] - params$package <- package - Sys.setenv("INPUT_REPORT_PKG_DIR" = package) - } else if (empty_pkg_info && is.empty(package)) { - stop("Package information missing for the report") - } else { - params$package <- package_name - } - - full_name <- paste0(package_name, "_v", package_version) - output_file <- paste0("validation_report_", full_name, ".qmd") - - params$package_name <- package_name - params$package_version <- package_version - params$image <- get_image_name(params) - - if (is.null(template_path) || !nzchar(template_path)) { - template_path <- system.file("report/package", - package = "val.report") - } else if (!dir.exists(template_path)) { - stop("Template directory is not available") - } - - params$package <- normalizePath(params$package, mustWork = FALSE, winslash = "/") - if (length(params$assessment_path) == 1L && !nzchar(params$assessment_path)) { - params$assessment_path <- normalizePath(params$assessment_path, mustWork = TRUE, winslash = "/") - } - - # Bug on https://github.com/quarto-dev/quarto-cli/issues/5765 - v <- quarto::quarto_version() - if (v < package_version("1.7.13")) { - warning("Please install the latest (devel) version of Quarto") - } - - if (is.null(params$source)) warning("Please provide the source of the package assessment") - - # https://github.com/quarto-dev/quarto-r/issues/81#issuecomment-1375691267 - # quarto rendering happens in the same place as the file/project - # To avoid issues copy to a different place and render there. - render_dir <- output_dir() + empty_pkg_info <- is.empty(package_name) && is.empty(package_version) + if (empty_pkg_info && !is.empty(package)) { + package_name <- basename(package) + desc <- read.dcf(file.path(package, "DESCRIPTION")) + + stopifnot( + "Mismatch between path and DESCRIPTION name" = package_name == + desc[, "Package"] + ) + package_version <- desc[, "Version"] + params$package <- package + Sys.setenv("INPUT_REPORT_PKG_DIR" = package) + } else if (empty_pkg_info && is.empty(package)) { + stop("Package information missing for the report") + } else { + params$package <- package_name + } + + full_name <- paste0(package_name, "_v", package_version) + output_file <- paste0("validation_report_", full_name, ".qmd") + + params$package_name <- package_name + params$package_version <- package_version + params$image <- get_image_name(params) + + if (is.null(template_path) || !nzchar(template_path)) { + template_path <- system.file("report/package", package = "val.report") + } else if (!dir.exists(template_path)) { + stop("Template directory is not available") + } + + params$package <- normalizePath( + params$package, + mustWork = FALSE, + winslash = "/" + ) + if (length(params$assessment_path) == 1L && !nzchar(params$assessment_path)) { + params$assessment_path <- normalizePath( + params$assessment_path, + mustWork = TRUE, + winslash = "/" + ) + } + + # Bug on https://github.com/quarto-dev/quarto-cli/issues/5765 + v <- quarto::quarto_version() + if (v < package_version("1.7.13")) { + warning("Please install the latest (devel) version of Quarto") + } + + if (is.null(params$source)) { + warning("Please provide the source of the package assessment") + } + + # https://github.com/quarto-dev/quarto-r/issues/81#issuecomment-1375691267 + # quarto rendering happens in the same place as the file/project + # To avoid issues copy to a different place and render there. + render_dir <- output_dir() + if (!dir.exists(render_dir)) { + render_dir <- paste0(render_dir, "/") if (!dir.exists(render_dir)) { - render_dir <- paste0(render_dir, "/") - if (!dir.exists(render_dir)) { - stop("Render directory doesn't exists. Please check the 'getOptions(\"valreport_output_dir\")' and sys.getEnv(\"VALREPORT_OUTPUT_DIR\")" ) - } - } - files_to_copy <- list.files(template_path, full.names = TRUE) - fc <- file.copy(from = files_to_copy, - to = render_dir, - overwrite = TRUE, - copy.date = TRUE) - - if (any(!fc)) { - stop("Copying to the rendering directory failed.") - } - - template_all_files <- list.files(render_dir, full.names = TRUE) - template <- template_all_files[endsWith(template_all_files, "qmd")] - - if (length(template) > 1) { - stop("There are more than one template!\n", - "Please have only one quarto file on the directory.") - } - - file_template <- file.path(render_dir, - paste0("validation_report_", full_name, ".qmd")) - file.rename(template, file_template) - - # replace the title of the place header by the package name and header - top_page_file <- readLines(file.path(render_dir, "top_page.html")) - title_line <- grep("
- as.character() - writeLines(top_page_file, file.path(render_dir, "top_page.html")) - - pre_rendering <- list.files(render_dir, full.names = TRUE) - - suppressMessages({suppressWarnings({ - out <- quarto::quarto_render( - input = file_template, - output_format = output_format, - execute_params = params, - ... + stop( + "Render directory doesn't exists. Please check the 'getOptions(\"valreport_output_dir\")' and sys.getEnv(\"VALREPORT_OUTPUT_DIR\")" ) - })}) + } + } + files_to_copy <- list.files(template_path, full.names = TRUE) + fc <- file.copy( + from = files_to_copy, + to = render_dir, + overwrite = TRUE, + copy.date = TRUE + ) + + if (any(!fc)) { + stop("Copying to the rendering directory failed.") + } + + template_all_files <- list.files(render_dir, full.names = TRUE) + template <- template_all_files[endsWith(template_all_files, "qmd")] + + if (length(template) > 1) { + stop( + "There are more than one template!\n", + "Please have only one quarto file on the directory." + ) + } + + file_template <- file.path( + render_dir, + paste0("validation_report_", full_name, ".qmd") + ) + file.rename(template, file_template) + + # replace the title of the place header by the package name and header + top_page_file <- readLines(file.path(render_dir, "top_page.html")) + title_line <- grep("
+ as.character() + writeLines(top_page_file, file.path(render_dir, "top_page.html")) + + pre_rendering <- list.files(render_dir, full.names = TRUE) + + out <- quarto::quarto_render( + input = file_template, + output_format = output_format, + execute_params = params, + ... + ) - post_rendering <- list.files(render_dir, full.names = TRUE) + post_rendering <- list.files(render_dir, full.names = TRUE) - files_to_remove <- intersect(pre_rendering, post_rendering) - fr <- file.remove(files_to_remove) - if (any(!fr)) { - warning("Failed to remove the quarto template used from the directory.") - } + files_to_remove <- intersect(pre_rendering, post_rendering) + fr <- file.remove(files_to_remove) + if (any(!fr)) { + warning("Failed to remove the quarto template used from the directory.") + } - output_files <- setdiff(post_rendering, pre_rendering) - invisible(output_files) + output_files <- setdiff(post_rendering, pre_rendering) + invisible(output_files) } is.empty <- function(x) { diff --git a/R/session.R b/R/session.R new file mode 100644 index 0000000..9894e76 --- /dev/null +++ b/R/session.R @@ -0,0 +1,23 @@ +#' Collect session information +#' +#' Catalogues characteristics of the R session and execution environment. +#' +#' @importFrom grDevices grSoftVersion +#' @importFrom utils sessionInfo +#' @export +session <- function() { + # NOTE: data that is provided as named character vectors are coerced to + # lists because the names otherwise get lost when passed as a quarto parameter + + list( + time = Sys.time(), + r_version = unclass(R.version), + session_info = sessionInfo(), + platform = .Platform, + machine = .Machine, + rng_kind = RNGkind(), + capabilities = as.list(capabilities()), + external_software = as.list(extSoftVersion()), + graphics_software = as.list(grSoftVersion()) + ) +} diff --git a/R/utils.R b/R/utils.R index 2918d7a..445bb64 100644 --- a/R/utils.R +++ b/R/utils.R @@ -54,7 +54,8 @@ is.empty <- function(x) { #' @details Please check `riskmetric::pkg_ref_class_hierarchy` and `riskmetric::pkg_ref` code #' to see the allowed values and structure get_pkg_origin <- function(source) { - source_values <- unlist(riskmetric:::pkg_ref_class_hierarchy) + pkg_ref_classes <- getNamespace("riskmetric")[["pkg_ref_class_hierarchy"]] + source_values <- unlist(pkg_ref_classes) report_source_values <- c("Missing Origin", "Source Package", "Internal Package", "CRAN", "Bioconductor", "Github") names(report_source_values) <- source_values report_source_values[source] diff --git a/R/utils_knitr.R b/R/utils_knitr.R new file mode 100644 index 0000000..106a67f --- /dev/null +++ b/R/utils_knitr.R @@ -0,0 +1,252 @@ +#' @importFrom utils packageName +roxygen2_knitr_note <- function() { + sprintf( + paste( + "`%s` `knitr` utilities are exported for use in", + "reports maintained by the R Validation Hub.", + "If you choose to use these functions for other purposes, be aware", + "that these are not considered stable for broader use." + ), + packageName() + ) +} + +#' Handler for complex option passing through from a quarto parameter +#' +#' Importantly, handles [`S7::new_class()`] objects which cannot be passed +#' through as a `quarto` command-line parameter because they can not be +#' deparsed. This helper allows passing arbitrary expressions using the +#' `!expr` prefix, which is standardized by `yaml`. +#' +#' @note `r roxygen2_knitr_note()` +#' +#' @param opts A `list` of options. For any non-serializable values that would +#' fail using your preferred interface to `knitr`, you may pass them as an +#' expression string such as `"!expr mtcars"`. Notably, +#' [`quarto::quarto_render`], which attempts to serialize objects for passing +#' to the command-line `quarto`, will fail for such objects and requires the +#' `"!expr"` prefix. +#' @param envir `environment` in which expressions should be evaluated. +#' +#' @return The `opts` list after parsing any complex expressions. This function +#' is used primarily for modifying the global state by calling [`options()`] +#' +#' @examples +#' # in a knitr document, we'll imagine we have some `params` object that we +#' # want to use to set options: +#' params <- list(options = list( +#' # most objects can be passed just fine +#' example_1 = list(a = 1L, b = "two"), +#' +#' # but objects that cannot be deserialized from their serialized (using, +#' # for example [`dput()`], [`dump()`], and [`deparse()`]) expression would +#' # fail as they are attempted to be passed to the `quarto` command-line +#' # utility. +#' complex_option = '!expr S7::new_class("example")()' +#' )) +#' +#' knitr_update_options(params$options) +#' +#' @export +knitr_update_options <- function(opts, envir = parent.frame()) { + opts <- as.list(opts) + opt_is_char <- vapply(opts, is.character, logical(1L)) + opt_is_expr <- opt_is_char + opt_is_expr[opt_is_char] <- startsWith( + as.character(opts[opt_is_char]), + "!expr" + ) + + opts[opt_is_expr] <- lapply( + opts[opt_is_expr], + function(expr) eval(parse(text = sub("^!expr", "", expr)), envir = envir) + ) + + do.call(options, opts) + opts +} + + +#' Create mutable header object +#' +#' Inject a custom document rendering hook into the knitr engine and return +#' a mutable environment that we can modify, which will be used to update the +#' knitr document header upon render completion. +#' +#' @note `r roxygen2_knitr_note()` +#' +#' @importFrom yaml yaml.load +#' @export +knitr_mutable_header <- function() { + if (!requireNamespace("knitr", quietly = TRUE)) { + warning("`knitr` is not available for use with knitr helpers") + return() + } + + header <- new.env(parent = emptyenv()) + + # add a hook that will replace the hard-coded front-matter with dynamic + # front-matter just before rendering. + knitr::knit_hooks$set( + document = local({ + default_document_hook <- knitr::knit_hooks$get("document") + function(x, options) { + # extract and split our document front-matter and body + x <- paste(x, collapse = "\n") + body <- sub("^\\s*(---|\\.\\.\\.).*?\\1", "", x, perl = TRUE) + fm <- substring(x, 0L, nchar(x) - nchar(body)) + + # pragmatically update front-matter at build-time + fm <- yaml::yaml.load(fm) + for (name in names(header)) { + fm[[name]] <- header[[name]] + } + + # rebuild our document + x <- paste0(c("---", yaml::as.yaml(fm), "---", body), collapse = "\n") + default_document_hook(x) + } + }) + ) + + header +} + +#' Various elements used for tailoring output style to command-line utility +knitr_logger_styles <- list( + knitr = list( + chunk_prefix = "\n", + line_prefix = " \u205A ", # vertical two dot punctuation + chunk_suffix = "" + ), + quarto = list( + chunk_prefix = "\n", + line_prefix = " ", + chunk_suffix = "\n" + ) +) + +#' Special handler for emitting knitr logs +#' +#' @inheritParams knitr::knit_print +#' @param output_style Choose which command-line output format to use when +#' logging. This will typically be set when a logger is created and should +#' not need to be manually provided. +#' +#' @importFrom utils head tail capture.output +#' @export +# nolint start +knit_print.knitr_log <- local({ + # nolint end + last_log_trailing_newline <- FALSE + style <- knitr_logger_styles + + # "progress" is used by knitr::knit function internally to store whether + # progress should be written to console, equivalent to !quiet + function(x, ..., output_style = c("knitr", "quarto")) { + quiet <- !knitr::opts_knit$get("progress") + if (quiet) { + return() + } + + output_style <- match.arg(output_style) + + # prefix newline only for the first message in each chunk + knitr_log_env <- environment(knitr_logger) + first_chunk_log <- knitr_log_env$first_chunk_log + knitr_log_env$first_chunk_log <- FALSE + + # split content on non-character (or AsIs) objects + is_char <- vapply(x, is.character, logical(1L)) + is_asis <- vapply(x, inherits, logical(1L), "AsIs") + is_char <- is_char & !is_asis + chunks <- cumsum( + !is_char | c(FALSE, tail(is_char, -1) & !head(is_char, -1)) + ) + is_char_chunk <- vapply(split(is_char, chunks), any, logical(1L)) + + # if output is a string, join them for pretty printing; otherwise capture + # console output for logging + x <- Map( + function(chunk, is_char) { + if (is_char) { + paste(chunk, collapse = "") + } else { + paste(capture.output(chunk[[1L]]), collapse = "\n") + } + }, + chunk = split(x, chunks), + is_char = is_char_chunk + ) + + # determine where to inject newline prefixes + x <- paste0(x, collapse = "") + x <- strsplit(x, "(?<=\n)", perl = TRUE)[[1L]] + prefixed <- if (first_chunk_log || last_log_trailing_newline) TRUE else -1L + x[prefixed] <- paste0(style[[output_style]]$line_prefix, x[prefixed]) + x <- paste0(x, collapse = "") + last_log_trailing_newline <<- endsWith(x[[length(x)]], "\n") + + # emit to stderr so that we see it immediately + if (first_chunk_log) { + cat(style[[output_style]]$chunk_prefix, file = stderr(), sep = "") + } + + cat(x, file = stderr(), sep = "") + } +}) + +#' Create a knitr log function +#' +#' Sets necessary knitr hooks and returns a logging function that will emit +#' messages to the console during knitting. +#' +#' @note `r roxygen2_knitr_note()` +#' +#' @return A `function` accepting `...` arguments, which will be used for +#' printing out to the console while rendering the knitr document. Character +#' values are logged directrly, while any other object is printed as though +#' being printed to the console. To render character values as they would +#' be printed, mark them as "AsIs" using [I()]. +#' +#' @export +knitr_logger <- local({ + first_chunk_log <- TRUE + style <- knitr_logger_styles + + function() { + if (!requireNamespace("knitr", quietly = TRUE)) { + warning("`knitr` is not available for use with knitr helpers") + return() + } + + # infer what what style to adopt for our console output + output_style <- "knitr" + if (nzchar(Sys.getenv("QUARTO_DOCUMENT_PATH"))) { + output_style <- "quarto" + } + + # reset our chunk start flag on chunk output + knitr::knit_hooks$set( + chunk = local({ + default_chunk_hook <- knitr::knit_hooks$get("chunk") + function(x, options) { + # if we've emitted any logs in this chunk, print the prefix + if (!first_chunk_log) { + cat(style[[output_style]]$chunk_suffix, file = stderr(), sep = "") + } + + first_chunk_log <<- TRUE + default_chunk_hook(x, options) + } + }) + ) + + function(...) { + knitr::knit_print( + structure(list(...), class = c("knitr_log", "list")), + output_style = output_style + ) + } + } +}) diff --git a/inst/report/package/pkg_template.qmd b/inst/report/package/pkg_template.qmd index 690b107..f4ced33 100644 --- a/inst/report/package/pkg_template.qmd +++ b/inst/report/package/pkg_template.qmd @@ -1,5 +1,5 @@ --- -title: "This report is fully automated and builds on [`r params$image`](`r paste('https://hub.docker.com/r', params$image, sep = '/')`) image." +title: "Validation Report" date-format: "YYYY-MM-DD hh:mm:ss" date: today published-title: "" @@ -49,6 +49,9 @@ knitr::opts_chunk$set( library("tools") library("knitr") library("val.report") + +header <- val.report::knitr_mutable_header() +log <- val.report::knitr_logger() ``` # Context @@ -58,6 +61,10 @@ library("val.report") pkg <- params$package pkg_name <- params$package_name pkg_version <- params$package_version + +log("Loaded package: ", pkg_name, " @ v", pkg_version, "\n") +header$title <- paste0(pkg_name, " (v", pkg_version, ")") + risk_path <- params$assessment_path image <- params$image ``` diff --git a/man/knit_print.knitr_log.Rd b/man/knit_print.knitr_log.Rd new file mode 100644 index 0000000..56ba133 --- /dev/null +++ b/man/knit_print.knitr_log.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knit_print.knitr_log} +\alias{knit_print.knitr_log} +\title{Special handler for emitting knitr logs} +\usage{ +knit_print.knitr_log(x, ..., output_style = c("knitr", "quarto")) +} +\arguments{ +\item{x}{An R object to be printed} + +\item{...}{Additional arguments passed to the S3 method. Currently ignored, +except two optional arguments \code{options} and \code{inline}; see +the references below.} + +\item{output_style}{Choose which command-line output format to use when +logging. This will typically be set when a logger is created and should +not need to be manually provided.} +} +\description{ +Special handler for emitting knitr logs +} diff --git a/man/knitr_logger.Rd b/man/knitr_logger.Rd new file mode 100644 index 0000000..b66b463 --- /dev/null +++ b/man/knitr_logger.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knitr_logger} +\alias{knitr_logger} +\title{Create a knitr log function} +\usage{ +knitr_logger() +} +\value{ +A \code{function} accepting \code{...} arguments, which will be used for +printing out to the console while rendering the knitr document. Character +values are logged directrly, while any other object is printed as though +being printed to the console. To render character values as they would +be printed, mark them as "AsIs" using \code{\link[=I]{I()}}. +} +\description{ +Sets necessary knitr hooks and returns a logging function that will emit +messages to the console during knitting. +} +\note{ +\code{val.report} \code{knitr} utilities are exported for use in reports maintained by the R Validation Hub. If you choose to use these functions for other purposes, be aware that these are not considered stable for broader use. +} diff --git a/man/knitr_logger_styles.Rd b/man/knitr_logger_styles.Rd new file mode 100644 index 0000000..9859a8d --- /dev/null +++ b/man/knitr_logger_styles.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\docType{data} +\name{knitr_logger_styles} +\alias{knitr_logger_styles} +\title{Various elements used for tailoring output style to command-line utility} +\format{ +An object of class \code{list} of length 2. +} +\usage{ +knitr_logger_styles +} +\description{ +Various elements used for tailoring output style to command-line utility +} +\keyword{datasets} diff --git a/man/knitr_mutable_header.Rd b/man/knitr_mutable_header.Rd new file mode 100644 index 0000000..6e0bda8 --- /dev/null +++ b/man/knitr_mutable_header.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knitr_mutable_header} +\alias{knitr_mutable_header} +\title{Create mutable header object} +\usage{ +knitr_mutable_header() +} +\description{ +Inject a custom document rendering hook into the knitr engine and return +a mutable environment that we can modify, which will be used to update the +knitr document header upon render completion. +} +\note{ +\code{val.report} \code{knitr} utilities are exported for use in reports maintained by the R Validation Hub. If you choose to use these functions for other purposes, be aware that these are not considered stable for broader use. +} diff --git a/man/knitr_update_options.Rd b/man/knitr_update_options.Rd new file mode 100644 index 0000000..e0ca853 --- /dev/null +++ b/man/knitr_update_options.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knitr_update_options} +\alias{knitr_update_options} +\title{Handler for complex option passing through from a quarto parameter} +\usage{ +knitr_update_options(opts, envir = parent.frame()) +} +\arguments{ +\item{opts}{A \code{list} of options. For any non-serializable values that would +fail using your preferred interface to \code{knitr}, you may pass them as an +expression string such as \code{"!expr mtcars"}. Notably, +\code{\link[quarto:quarto_render]{quarto::quarto_render}}, which attempts to serialize objects for passing +to the command-line \code{quarto}, will fail for such objects and requires the +\code{"!expr"} prefix.} + +\item{envir}{\code{environment} in which expressions should be evaluated.} +} +\value{ +The \code{opts} list after parsing any complex expressions. This function +is used primarily for modifying the global state by calling \code{\link[=options]{options()}} +} +\description{ +Importantly, handles \code{\link[S7:new_class]{S7::new_class()}} objects which cannot be passed +through as a \code{quarto} command-line parameter because they can not be +deparsed. This helper allows passing arbitrary expressions using the +\code{!expr} prefix, which is standardized by \code{yaml}. +} +\note{ +\code{val.report} \code{knitr} utilities are exported for use in reports maintained by the R Validation Hub. If you choose to use these functions for other purposes, be aware that these are not considered stable for broader use. +} +\examples{ +# in a knitr document, we'll imagine we have some `params` object that we +# want to use to set options: +params <- list(options = list( + # most objects can be passed just fine + example_1 = list(a = 1L, b = "two"), + + # but objects that cannot be deserialized from their serialized (using, + # for example [`dput()`], [`dump()`], and [`deparse()`]) expression would + # fail as they are attempted to be passed to the `quarto` command-line + # utility. + complex_option = '!expr S7::new_class("example")()' +)) + +knitr_update_options(params$options) + +} diff --git a/man/package_report.Rd b/man/package_report.Rd index 9b69335..debaf52 100644 --- a/man/package_report.Rd +++ b/man/package_report.Rd @@ -19,9 +19,11 @@ package_report( \item{package_version}{Package version number.} -\item{package}{Path where to find a package source to retrieve name and version number.} +\item{package}{Path where to find a package source to retrieve name and +version number.} -\item{template_path}{Path to a directory with one quarto template file (and the files required for rendering it).} +\item{template_path}{Path to a directory with one quarto template file (and +the files required for rendering it).} \item{output_format}{Output format for the report. Default is "all".} @@ -36,8 +38,9 @@ A path to the reports generated, called by its side effects. Reports about a package } \details{ -Please include source as part of \code{params} content. Source is returned after -calling function \code{riskmetric::pkg_ref} before the risk assessment is executed +Please include source as part of \code{params} content. Source is +returned after calling function \code{riskmetric::pkg_ref} before the risk +assessment is executed } \examples{ options("valreport_output_dir" = tempdir()) @@ -45,10 +48,12 @@ pr <- package_report( package_name = "dplyr", package_version = "1.1.4", params = list( - assessment_path = system.file("assessments/dplyr.rds", package = "val.report"), + assessment_path = + system.file("assessments/dplyr.rds", package = "val.report"), image = "rhub/ref-image"), quiet = FALSE ) pr file.remove(pr) + } diff --git a/man/session.Rd b/man/session.Rd new file mode 100644 index 0000000..f0c5b08 --- /dev/null +++ b/man/session.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/session.R +\name{session} +\alias{session} +\title{Collect session information} +\usage{ +session() +} +\description{ +Catalogues characteristics of the R session and execution environment. +} diff --git a/tests/testthat/test-utils-knitr.R b/tests/testthat/test-utils-knitr.R new file mode 100644 index 0000000..2296120 --- /dev/null +++ b/tests/testthat/test-utils-knitr.R @@ -0,0 +1,153 @@ +example_1 <- list(a = 8L, b = "bee") +example_2 <- S7::new_class("test", package = "example")() +example_2_yaml_expr <- '!expr S7::new_class("test", package = "example")()' + +params_r <- list( + options = list( + example_1 = example_1, + example_2 = example_2 + ) +) + +params_expr <- list( + options = list( + example_1 = example_1, + example_2 = example_2_yaml_expr + ) +) + +example_qmd <- tempfile(fileext = ".qmd") +example_rds <- tempfile(fileext = ".Rds") +example_parsed_options <- function() readRDS(example_rds) + +writeLines( + con = example_qmd, + sprintf( + ' +--- +title: Example +params: { options: NULL } +--- + +```{r, echo = FALSE} +library(val.report) + +# logging +log <- knitr_logger() +log("test 1 2 3\n") + +# update header +header <- knitr_mutable_header() +header$title <- "Mutable Title" + +# update and save out options +opts <- knitr_update_options(params$options) +saveRDS(opts, "%s") +```', + example_rds + ) +) + +test_that("knitr_update_options can accept complex R objects", { + opts <- options() + on.exit(options(opts)) + + expect_silent(knitr_update_options(params_r$options)) + expect_identical(getOption("example_1"), example_1) + expect_identical(getOption("example_2"), example_2) +}) + +test_that( + paste( + "knitr_update_options accepts complex R objects through", + "rmarkdown::render" + ), + { + skip_if_not_installed("rmarkdown") + + # this would work even without our function by just setting + # `options(params$options)`. However, we want to make sure it still works + # when it gets passed through our helper + rmarkdown::render(example_qmd, params = params_r, quiet = TRUE) + parsed_options <- example_parsed_options() + + expect_identical(parsed_options$example_2, example_2) + } +) + +test_that("knitr_update_options is still required for quarto::quarto_render", { + skip_if_not_installed("quarto") + skip_if( + is.null(quarto::quarto_path()), + "system `quarto` executable unavailable" + ) + + # ignore this test on cran because it is testing behavior of another package + skip_on_cran() + + # if this test starts to fail, it probably means that S7 has found a solution + # to serializing their objects, removing the need for `knitr_update_options` + # + # See: + # - https://github.com/RConsortium/S7/issues/225 + # - https://github.com/RConsortium/S7/issues/524 + # + quarto::quarto_render(example_qmd, execute_params = params_r) |> + expect_error("not implemented.*'object'") |> + expect_warning("'object'") +}) + +test_that( + paste( + "knitr_update_options accepts complex R objects through", + "quarto::quarto_render" + ), + { + skip_if_not_installed("quarto") + skip_if( + is.null(quarto::quarto_path()), + "system `quarto` executable unavailable" + ) + + quarto::quarto_render( + example_qmd, + execute_params = params_expr, + quiet = TRUE + ) |> + expect_silent() + + parsed_options <- example_parsed_options() + expect_identical(parsed_options$example_2, example_2) + } +) + +test_that("knitr_mutable_header can modify yaml frontmatter during runtime", { + skip_if_not_installed("rmarkdown") + + rmarkdown::render( + example_qmd, + # pass content that will be used to modify the yaml frontmatter at runtime + output_file = example_md <- tempfile(fileext = ".md"), + output_format = rmarkdown::github_document(), + quiet = TRUE + ) + + content <- readLines(example_md) + expect_match(paste(content, collapse = "\n"), "^Mutable Title\n===", ) +}) + +test_that("knitr_logger writes to console output", { + skip_if_not_installed("rmarkdown") + + output <- capture.output({ + messages <- capture.output(type = "message", { + rmarkdown::render( + example_qmd, + output_file = example_md <- tempfile(fileext = ".md"), + output_format = rmarkdown::github_document() + ) + }) + }) + + expect_true(any(grepl("test 1 2 3", messages))) +})