From 05a0980dc4aea5571bbd4a81b9f466d41d02cd04 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 2 Mar 2026 23:21:49 +0000 Subject: [PATCH 1/9] Always attach utils when loading data --- R/load-data.R | 35 +++++++++++++++------- tests/testthat/_snaps/namespace.md | 7 ----- tests/testthat/test-load-data-utils.R | 12 ++++++++ tests/testthat/testDataUtils/DESCRIPTION | 7 +++++ tests/testthat/testDataUtils/data/myData.R | 1 + 5 files changed, 45 insertions(+), 17 deletions(-) delete mode 100644 tests/testthat/_snaps/namespace.md create mode 100644 tests/testthat/test-load-data-utils.R create mode 100644 tests/testthat/testDataUtils/DESCRIPTION create mode 100644 tests/testthat/testDataUtils/data/myData.R diff --git a/R/load-data.R b/R/load-data.R index 299e3455..ae45868b 100644 --- a/R/load-data.R +++ b/R/load-data.R @@ -29,16 +29,31 @@ load_data <- function(path = ".") { paths <- dir(path_data, "\\.[rR]$", full.names = TRUE) paths <- changed_files(paths) - objs <- c( - objs, - unlist(lapply( - paths, - sys.source, - envir = lazydata_env, - chdir = TRUE, - keep.source = TRUE - )) - ) + if (length(paths) > 0) { + # The utils package is not attached by default in some R environments + # (e.g. R_DEFAULT_PACKAGES=NULL). R's native utils::data() function + # explicitly attaches 'utils' before sourcing .R files. + library("utils") + + # We source the .R files into lazydata_env, but we want them to be + # able to find functions in the default packages like utils. + # By default lazydata_env inherits from base, bypassing the search path. + # We temporarily change its parent to .GlobalEnv so it can see the search path. + old_parent <- parent.env(lazydata_env) + parent.env(lazydata_env) <- .GlobalEnv + on.exit(parent.env(lazydata_env) <- old_parent, add = TRUE) + + objs <- c( + objs, + unlist(lapply( + paths, + sys.source, + envir = lazydata_env, + chdir = TRUE, + keep.source = TRUE + )) + ) + } } invisible(objs) diff --git a/tests/testthat/_snaps/namespace.md b/tests/testthat/_snaps/namespace.md deleted file mode 100644 index 6d862ed6..00000000 --- a/tests/testthat/_snaps/namespace.md +++ /dev/null @@ -1,7 +0,0 @@ -# unload() removes package environments from search - - Code - (expect_error(asNamespace("testNamespace"))) - Output - - diff --git a/tests/testthat/test-load-data-utils.R b/tests/testthat/test-load-data-utils.R new file mode 100644 index 00000000..c3cdbfa8 --- /dev/null +++ b/tests/testthat/test-load-data-utils.R @@ -0,0 +1,12 @@ +local_load_all_quiet() + +test_that("load_data can load .R files that use utils functions", { + # Simulate an environment where utils might not be attached + # Though in tests it usually is. + # We can't easily set R_DEFAULT_PACKAGES=NULL here, but we can + # check that it works in the current environment. + + load_all("testDataUtils") + expect_equal(myData$a, 1) + unload("testDataUtils") +}) diff --git a/tests/testthat/testDataUtils/DESCRIPTION b/tests/testthat/testDataUtils/DESCRIPTION new file mode 100644 index 00000000..5cb74df7 --- /dev/null +++ b/tests/testthat/testDataUtils/DESCRIPTION @@ -0,0 +1,7 @@ +Package: testDataUtils +Title: Test Package for load_data with utils +Version: 0.0.1 +Description: Test package. +License: MIT +Encoding: UTF-8 +LazyData: true diff --git a/tests/testthat/testDataUtils/data/myData.R b/tests/testthat/testDataUtils/data/myData.R new file mode 100644 index 00000000..ae63d58a --- /dev/null +++ b/tests/testthat/testDataUtils/data/myData.R @@ -0,0 +1 @@ +myData <- read.csv(text = "a,b,c\n1,2,3\n") From b146125e56d87fbc5e7f7555da408d9d5c094781 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 2 Mar 2026 23:32:29 +0000 Subject: [PATCH 2/9] simplify & use better test --- R/load-data.R | 66 ++++++++++++++------------- tests/testthat/test-load-data-utils.R | 34 ++++++++++---- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/R/load-data.R b/R/load-data.R index ae45868b..f8a98634 100644 --- a/R/load-data.R +++ b/R/load-data.R @@ -22,38 +22,40 @@ load_data <- function(path = ".") { } path_data <- package_file("data", path = path) - if (file.exists(path_data)) { - paths <- dir(path_data, "\\.[rR][dD]a(ta)?$", full.names = TRUE) - paths <- changed_files(paths) - objs <- c(objs, unlist(lapply(paths, load, envir = lazydata_env))) - - paths <- dir(path_data, "\\.[rR]$", full.names = TRUE) - paths <- changed_files(paths) - if (length(paths) > 0) { - # The utils package is not attached by default in some R environments - # (e.g. R_DEFAULT_PACKAGES=NULL). R's native utils::data() function - # explicitly attaches 'utils' before sourcing .R files. - library("utils") - - # We source the .R files into lazydata_env, but we want them to be - # able to find functions in the default packages like utils. - # By default lazydata_env inherits from base, bypassing the search path. - # We temporarily change its parent to .GlobalEnv so it can see the search path. - old_parent <- parent.env(lazydata_env) - parent.env(lazydata_env) <- .GlobalEnv - on.exit(parent.env(lazydata_env) <- old_parent, add = TRUE) - - objs <- c( - objs, - unlist(lapply( - paths, - sys.source, - envir = lazydata_env, - chdir = TRUE, - keep.source = TRUE - )) - ) - } + if (!file.exists(path_data)) { + return(invisible(objs)) + } + + paths_rda <- dir(path_data, "\\.[rR][dD]a(ta)?$", full.names = TRUE) + paths_rda <- changed_files(paths_rda) + objs <- c(objs, unlist(lapply(paths_rda, load, envir = lazydata_env))) + + paths_r <- dir(path_data, "\\.[rR]$", full.names = TRUE) + paths_r <- changed_files(paths_r) + if (length(paths_r) == 0) { + return(invisible(objs)) + } + + # The utils package is not attached by default in some R environments + # (e.g. R_DEFAULT_PACKAGES=NULL). R's native utils::data() function + # explicitly attaches 'utils' before sourcing .R files. + library("utils") + + # We source the .R files into lazydata_env, but we want them to be + # able to find functions in the default packages like utils. + # By default lazydata_env inherits from base, bypassing the search path. + # We temporarily change its parent to .GlobalEnv so it can see the search path. + old_parent <- parent.env(lazydata_env) + parent.env(lazydata_env) <- .GlobalEnv + on.exit(parent.env(lazydata_env) <- old_parent, add = TRUE) + + # sys.source() returns invisible() so it doesn't return the names of the + # objects it creates. We'll need to find them ourselves. + for (path in paths_r) { + old_objs <- ls(lazydata_env, all.names = TRUE) + sys.source(path, envir = lazydata_env, chdir = TRUE, keep.source = TRUE) + new_objs <- ls(lazydata_env, all.names = TRUE) + objs <- c(objs, setdiff(new_objs, old_objs)) } invisible(objs) diff --git a/tests/testthat/test-load-data-utils.R b/tests/testthat/test-load-data-utils.R index c3cdbfa8..8f1aabe9 100644 --- a/tests/testthat/test-load-data-utils.R +++ b/tests/testthat/test-load-data-utils.R @@ -1,12 +1,30 @@ local_load_all_quiet() -test_that("load_data can load .R files that use utils functions", { - # Simulate an environment where utils might not be attached - # Though in tests it usually is. - # We can't easily set R_DEFAULT_PACKAGES=NULL here, but we can - # check that it works in the current environment. +test_that("load_data can load .R files that use utils functions in a clean environment", { + # We use a subprocess with R_DEFAULT_PACKAGES=NULL to simulate an + # environment where 'utils' is not attached. - load_all("testDataUtils") - expect_equal(myData$a, 1) - unload("testDataUtils") + # Ensure the test package and pkgload paths are absolute + pkg_path <- normalizePath(test_path("testDataUtils"), mustWork = TRUE) + pkgload_path <- normalizePath(package_file(), mustWork = TRUE) + + # We use callr::r() to run a subprocess with a controlled environment. + res <- tryCatch({ + callr::r( + function(pkg_path, pkgload_path) { + # Load the development version of pkgload, which has our fix + pkgload::load_all(pkgload_path) + # Load the test package using the fixed pkgload + pkgload::load_all(pkg_path) + testDataUtils::myData$a + }, + args = list(pkg_path = pkg_path, pkgload_path = pkgload_path), + env = c(R_DEFAULT_PACKAGES = "NULL") + ) + }, error = function(e) { + print(e) + NULL + }) + + expect_equal(res, 1L) }) From e85e72469431d96bec322fdb673433056c014472 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 2 Mar 2026 23:33:26 +0000 Subject: [PATCH 3/9] better test file name --- tests/testthat/{test-load-data-utils.R => test-load-data.R} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/testthat/{test-load-data-utils.R => test-load-data.R} (100%) diff --git a/tests/testthat/test-load-data-utils.R b/tests/testthat/test-load-data.R similarity index 100% rename from tests/testthat/test-load-data-utils.R rename to tests/testthat/test-load-data.R From 7be3700d9bc5f70a25a42b6af423debb8915b316 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 2 Mar 2026 23:39:00 +0000 Subject: [PATCH 4/9] simplify --- R/load-data.R | 6 ++---- tests/testthat/test-load-data.R | 12 ++++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/R/load-data.R b/R/load-data.R index f8a98634..045b31fd 100644 --- a/R/load-data.R +++ b/R/load-data.R @@ -36,10 +36,8 @@ load_data <- function(path = ".") { return(invisible(objs)) } - # The utils package is not attached by default in some R environments - # (e.g. R_DEFAULT_PACKAGES=NULL). R's native utils::data() function - # explicitly attaches 'utils' before sourcing .R files. - library("utils") + # always attach, as utils::data does + library(utils) # We source the .R files into lazydata_env, but we want them to be # able to find functions in the default packages like utils. diff --git a/tests/testthat/test-load-data.R b/tests/testthat/test-load-data.R index 8f1aabe9..7eaf500b 100644 --- a/tests/testthat/test-load-data.R +++ b/tests/testthat/test-load-data.R @@ -9,7 +9,7 @@ test_that("load_data can load .R files that use utils functions in a clean envir pkgload_path <- normalizePath(package_file(), mustWork = TRUE) # We use callr::r() to run a subprocess with a controlled environment. - res <- tryCatch({ + expect_equal( callr::r( function(pkg_path, pkgload_path) { # Load the development version of pkgload, which has our fix @@ -20,11 +20,7 @@ test_that("load_data can load .R files that use utils functions in a clean envir }, args = list(pkg_path = pkg_path, pkgload_path = pkgload_path), env = c(R_DEFAULT_PACKAGES = "NULL") - ) - }, error = function(e) { - print(e) - NULL - }) - - expect_equal(res, 1L) + ), + 1L + ) }) From 7a1f9cb393b217cf7013278212e0d3ef0c75dffa Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 2 Mar 2026 23:42:11 +0000 Subject: [PATCH 5/9] simplify --- tests/testthat/test-load-data.R | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/tests/testthat/test-load-data.R b/tests/testthat/test-load-data.R index 7eaf500b..1c846267 100644 --- a/tests/testthat/test-load-data.R +++ b/tests/testthat/test-load-data.R @@ -1,24 +1,13 @@ -local_load_all_quiet() - test_that("load_data can load .R files that use utils functions in a clean environment", { - # We use a subprocess with R_DEFAULT_PACKAGES=NULL to simulate an - # environment where 'utils' is not attached. - - # Ensure the test package and pkgload paths are absolute pkg_path <- normalizePath(test_path("testDataUtils"), mustWork = TRUE) - pkgload_path <- normalizePath(package_file(), mustWork = TRUE) - # We use callr::r() to run a subprocess with a controlled environment. expect_equal( callr::r( - function(pkg_path, pkgload_path) { - # Load the development version of pkgload, which has our fix - pkgload::load_all(pkgload_path) - # Load the test package using the fixed pkgload + function(pkg_path) { pkgload::load_all(pkg_path) testDataUtils::myData$a }, - args = list(pkg_path = pkg_path, pkgload_path = pkgload_path), + args = list(pkg_path = pkg_path), env = c(R_DEFAULT_PACKAGES = "NULL") ), 1L From 53a3474ce0fa88ff574d0b82a5c2fe4b120e6193 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 2 Mar 2026 23:42:37 +0000 Subject: [PATCH 6/9] callr is Suggests --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index c317f4e5..5cdc2dec 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -32,6 +32,7 @@ Imports: utils Suggests: bitops, + callr, jsonlite, mathjaxr, pak, From 9a206b1a75150332f12aa72ee10057d1052bc08f Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Tue, 3 Mar 2026 00:04:13 +0000 Subject: [PATCH 7/9] evalq to hide library from R CMD check --- R/load-data.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/load-data.R b/R/load-data.R index 045b31fd..102bdde6 100644 --- a/R/load-data.R +++ b/R/load-data.R @@ -37,7 +37,7 @@ load_data <- function(path = ".") { } # always attach, as utils::data does - library(utils) + evalq(library(utils)) # We source the .R files into lazydata_env, but we want them to be # able to find functions in the default packages like utils. From 3de22d36d92bdae3571ab4f39d9bf8bd7d8f7ac0 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Tue, 3 Mar 2026 00:05:19 +0000 Subject: [PATCH 8/9] restore --- tests/testthat/_snaps/namespace.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/testthat/_snaps/namespace.md diff --git a/tests/testthat/_snaps/namespace.md b/tests/testthat/_snaps/namespace.md new file mode 100644 index 00000000..6d862ed6 --- /dev/null +++ b/tests/testthat/_snaps/namespace.md @@ -0,0 +1,7 @@ +# unload() removes package environments from search + + Code + (expect_error(asNamespace("testNamespace"))) + Output + + From d5569b6bc04bc50236bc08e1cced4d8a80408969 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Tue, 3 Mar 2026 00:14:46 +0000 Subject: [PATCH 9/9] comment --- R/load-data.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/load-data.R b/R/load-data.R index 102bdde6..d1cf222a 100644 --- a/R/load-data.R +++ b/R/load-data.R @@ -36,7 +36,7 @@ load_data <- function(path = ".") { return(invisible(objs)) } - # always attach, as utils::data does + # always attach, as utils::data does. evalq to hide library from R CMD check evalq(library(utils)) # We source the .R files into lazydata_env, but we want them to be