Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement local_reproducible_output() #190

Merged
merged 7 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export(is.message)
export(is.recordedplot)
export(is.source)
export(is.warning)
export(local_reproducible_output)
export(new_output_handler)
export(parse_all)
export(remove_hooks)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# evaluate (development version)

* New `local_reproducible_output()` helper that sets various options and env vars to help ensure consistency of output across environments.
* The `source` output handler is now passed the entire top-level expression, not just the first component.
* `evaluate()` will now terminate on the first error in a top-level expression. This matches R's own behaviour more closely.
* `is.value()` has been removed since it tests for an object that evaluate never creates.
Expand Down
112 changes: 112 additions & 0 deletions R/reproducible-output.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#' Control common output options
#'
#' @description
#' Often when using `evaluate()` you are running R code with a specific output
#' context in mind. But there are many options and env vars that packages
#' will take from the current environment, meaning that output depends on
#' the current state in undesirable ways.
#'
#' This function allows you to describe the characteristics of the desired
#' output and takes care of setting the options and environment variables
#' for you.
#'
#' @export
#' @param width Value of the `"width"` option.
#' @param color Determines whether or not cli/crayon colour should be used.
#' @param unicode Should we use unicode characaters where possible?
#' @param hyperlinks Should we use ANSI hyperlinks?
#' @param rstudio Should we pretend that we're running inside of RStudio?
#' @param frame Scope of the changes; when this calling frame terminates the
#' changes will be undone. For expert use only.
local_reproducible_output <- function(width = 80,
color = FALSE,
unicode = FALSE,
hyperlinks = FALSE,
rstudio = FALSE,
frame = parent.frame()) {

local_options(
# crayon
crayon.enabled = color,

# cli
cli.width = width,
cli.condition_width = width,
cli.num_colors = if (color) 8L else 1L,
cli.hyperlink = hyperlinks,
cli.hyperlink_run = hyperlinks,
cli.hyperlink_help = hyperlinks,
cli.hyperlink_vignette = hyperlinks,
cli.unicode = unicode,
cli.dynamic = FALSE,

# base R
width = width,
useFancyQuotes = unicode,

# rlang
rlang_interactive = FALSE,

.frame = frame
)

local_envvar(
NO_COLOR = if (color) NA else 1,

# Simulate RStudio
RSTUDIO = if (rstudio) 1 else NA,
RSTUDIO_SESSION_PID = if (rstudio) Sys.getpid() else NA,
RSTUDIO_CHILD_PROCESS_PANE = if (rstudio) "build" else NA,
RSTUDIO_CLI_HYPERLINKS = if (rstudio) 1 else NA,
RSTUDIO_CONSOLE_WIDTH = width,
.frame = frame
)

local_collate("C", frame = frame)

invisible()
}

local_options <- function(..., .frame = parent.frame()) {
old <- options(...)
defer(options(old), .frame)

invisible()
}

local_envvar <- function(..., .frame = parent.frame()) {
old <- set_envvar(list(...))
defer(set_envvar(old), .frame)

invisible()
}

local_collate <- function(locale, frame = parent.frame()) {
hadley marked this conversation as resolved.
Show resolved Hide resolved
old <- Sys.getlocale("LC_COLLATE")
defer(Sys.setlocale("LC_COLLATE", old), frame)
Sys.setlocale("LC_COLLATE", locale)

# From https://github.com/r-lib/withr/blob/v3.0.0/R/locale.R#L51-L55:
# R supports setting LC_COLLATE to C via envvar. When that is the
# case, it takes precedence over the currently set locale. We need
# to set both the envvar and the locale for collate to fully take
# effect.
local_envvar(LC_COLLATE = locale, .frame = frame)

invisible()
}

# adapted from withr:::set_envvar
set_envvar <- function(envs) {
if (length(envs) == 0) {
return()

Check warning on line 102 in R/reproducible-output.R

View check run for this annotation

Codecov / codecov/patch

R/reproducible-output.R#L102

Added line #L102 was not covered by tests
}

old <- Sys.getenv(names(envs), names = TRUE, unset = NA)
set <- !is.na(envs)

if (any(set)) do.call("Sys.setenv", as.list(envs[set]))
if (any(!set)) Sys.unsetenv(names(envs)[!set])

invisible(old)
}
39 changes: 39 additions & 0 deletions man/local_reproducible_output.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions tests/testthat/test-reproducible-output.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
test_that("local_reproducible_output() respects local context", {

local_reproducible_output(width = 105)
expect_equal(getOption("width"), 105)

local({
local_reproducible_output(width = 110)
expect_equal(getOption("width"), 110)
})

expect_equal(getOption("width"), 105)
})

test_that("local_envvar respects local context", {
local_envvar(test = "a")
expect_equal(Sys.getenv("test"), "a")

local({
local_envvar(test = "b")
expect_equal(Sys.getenv("test"), "b")
})

expect_equal(Sys.getenv("test"), "a")
local({
local_envvar(test = NA)
expect_equal(Sys.getenv("test"), "")
})

expect_equal(Sys.getenv("test"), "a")
})

test_that("local_collate respects local context", {
locale <- switch(Sys.info()[["sysname"]],
Darwin = "en_US",
Linux = "en_US.UTF-8",
NULL
)
skip_if(is.null(locale), "Don't know good locale to use for this platform")

local_collate("C")
expect_equal(Sys.getlocale("LC_COLLATE"), "C")

local({
local_collate(locale)
expect_equal(Sys.getlocale("LC_COLLATE"), locale)
})

expect_equal(Sys.getlocale("LC_COLLATE"), "C")
})
Loading