From 78d389207fe2a293b6a0553d74917d485c56c306 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Wed, 4 Feb 2026 15:48:06 +0100 Subject: [PATCH 1/3] set reticulate env in py2r to avoid numpy issues --- R/rxp_io.R | 37 ++++++++++---- man/rxp_py2r.Rd | 5 +- man/rxp_r2py.Rd | 5 +- tests/testthat/test-reticulate-config.R | 67 +++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 tests/testthat/test-reticulate-config.R diff --git a/R/rxp_io.R b/R/rxp_io.R index 36c01de..0fc4d94 100644 --- a/R/rxp_io.R +++ b/R/rxp_io.R @@ -644,12 +644,25 @@ rxp_jl_file <- function(...) rxp_file("Jl", ...) #' @return A list with elements: `name`, `snippet`, `type`, `additional_files`, #' `nix_env`. #' @noRd -rxp_common_setup <- function(out_name, expr_str, nix_env, direction) { +rxp_common_setup <- function(out_name, expr_str, nix_env, direction, env_var = NULL) { expr_str <- gsub("\"", "'", expr_str) base <- sanitize_nix_env(nix_env) + # Always set RETICULATE_AUTOCONFIGURE=0 to prevent reticulate from + # modifying PYTHONPATH in Nix's hermetic build environment + default_env <- c(RETICULATE_AUTOCONFIGURE = "0") + + # Merge with user-provided env_var if present (user values take precedence) + if (!is.null(env_var) && length(env_var) > 0) { + env_var <- c(default_env[!names(default_env) %in% names(env_var)], env_var) + } else { + env_var <- default_env + } + + env_exports <- build_env_exports(env_var) + r_command <- build_transfer_command(out_name, expr_str, direction) - build_phase <- build_reticulate_phase(r_command) + build_phase <- build_reticulate_phase(r_command, env_exports) snippet <- make_derivation_snippet( out_name = out_name, @@ -665,7 +678,8 @@ rxp_common_setup <- function(out_name, expr_str, nix_env, direction) { snippet = snippet, type = paste0("rxp_", direction), additional_files = "", - nix_env = nix_env + nix_env = nix_env, + env_var = env_var # Store for potential inspection/debugging ), class = "rxp_derivation" ) @@ -704,9 +718,10 @@ build_transfer_command <- function(out_name, expr_str, direction) { #' @param r_command Character R command #' @return Character build phase #' @noRd -build_reticulate_phase <- function(r_command) { +build_reticulate_phase <- function(r_command, env_exports = "") { sprintf( - "export RETICULATE_PYTHON=${defaultPkgs.python3}/bin/python\n Rscript -e \"\n source('libraries.R')\n%s\"", + "export RETICULATE_PYTHON=${defaultPkgs.python3}/bin/python\n %sRscript -e \"\n source('libraries.R')\n%s\"", + env_exports, r_command ) } @@ -718,14 +733,16 @@ build_reticulate_phase <- function(r_command) { #' @param expr Symbol, Python object to be loaded into R. #' @param nix_env Character, path to the Nix environment file, default is #' "default.nix". +#' @param env_var Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +#' is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds. #' @details `rxp_py2r(my_obj, my_python_object)` loads a serialized Python #' object and saves it as an RDS file using `reticulate::py_load_object()`. #' @return An object of class `rxp_derivation`. #' @export -rxp_py2r <- function(name, expr, nix_env = "default.nix") { +rxp_py2r <- function(name, expr, nix_env = "default.nix", env_var = NULL) { out_name <- deparse1(substitute(name)) expr_str <- deparse1(substitute(expr)) - rxp_common_setup(out_name, expr_str, nix_env, "py2r") + rxp_common_setup(out_name, expr_str, nix_env, "py2r", env_var) } #' Transfer R Object into a Python Session @@ -735,12 +752,14 @@ rxp_py2r <- function(name, expr, nix_env = "default.nix") { #' @param expr Symbol, R object to be saved into a Python pickle. #' @param nix_env Character, path to the Nix environment file, default is #' "default.nix". +#' @param env_var Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +#' is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds. #' @details `rxp_r2py(my_obj, my_r_object)` saves an R object to a Python pickle #' using `reticulate::py_save_object()`. #' @return An object of class `rxp_derivation`. #' @export -rxp_r2py <- function(name, expr, nix_env = "default.nix") { +rxp_r2py <- function(name, expr, nix_env = "default.nix", env_var = NULL) { out_name <- deparse1(substitute(name)) expr_str <- deparse1(substitute(expr)) - rxp_common_setup(out_name, expr_str, nix_env, "r2py") + rxp_common_setup(out_name, expr_str, nix_env, "r2py", env_var) } diff --git a/man/rxp_py2r.Rd b/man/rxp_py2r.Rd index b0dc5db..211c366 100644 --- a/man/rxp_py2r.Rd +++ b/man/rxp_py2r.Rd @@ -4,7 +4,7 @@ \alias{rxp_py2r} \title{Transfer Python Object into an R Session} \usage{ -rxp_py2r(name, expr, nix_env = "default.nix") +rxp_py2r(name, expr, nix_env = "default.nix", env_var = NULL) } \arguments{ \item{name}{Symbol, name of the derivation.} @@ -13,6 +13,9 @@ rxp_py2r(name, expr, nix_env = "default.nix") \item{nix_env}{Character, path to the Nix environment file, default is "default.nix".} + +\item{env_var}{Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds.} } \value{ An object of class \code{rxp_derivation}. diff --git a/man/rxp_r2py.Rd b/man/rxp_r2py.Rd index b64af0c..2e7ab20 100644 --- a/man/rxp_r2py.Rd +++ b/man/rxp_r2py.Rd @@ -4,7 +4,7 @@ \alias{rxp_r2py} \title{Transfer R Object into a Python Session} \usage{ -rxp_r2py(name, expr, nix_env = "default.nix") +rxp_r2py(name, expr, nix_env = "default.nix", env_var = NULL) } \arguments{ \item{name}{Symbol, name of the derivation.} @@ -13,6 +13,9 @@ rxp_r2py(name, expr, nix_env = "default.nix") \item{nix_env}{Character, path to the Nix environment file, default is "default.nix".} + +\item{env_var}{Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds.} } \value{ An object of class \code{rxp_derivation}. diff --git a/tests/testthat/test-reticulate-config.R b/tests/testthat/test-reticulate-config.R new file mode 100644 index 0000000..6eeae0f --- /dev/null +++ b/tests/testthat/test-reticulate-config.R @@ -0,0 +1,67 @@ +test_that("rxp_py2r sets RETICULATE_AUTOCONFIGURE=0 by default", { + # Mock user input + nix_env <- "default.nix" + + # Default behavior + res <- rxp_py2r( + name = my_obj, + expr = py_obj, + nix_env = nix_env + ) + + expect_true(!is.null(res$env_var)) + expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") + + # Verify it appears in the snippet (indirectly validating build_reticulate_phase logic if it was simple string matching) + # But better to check the structured 'env_var' component we added. +}) + +test_that("rxp_r2py sets RETICULATE_AUTOCONFIGURE=0 by default", { + nix_env <- "default.nix" + + res <- rxp_r2py( + name = py_obj, + expr = r_obj, + nix_env = nix_env + ) + + expect_true(!is.null(res$env_var)) + expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") +}) + +test_that("User can override RETICULATE_AUTOCONFIGURE", { + nix_env <- "default.nix" + + # Override to "1" + res_override <- rxp_py2r( + name = my_obj, + expr = py_obj, + nix_env = nix_env, + env_var = c(RETICULATE_AUTOCONFIGURE = "1") + ) + + expect_equal(res_override$env_var[["RETICULATE_AUTOCONFIGURE"]], "1") + + # Override to empty string/something else + res_override2 <- rxp_r2py( + name = py_obj, + expr = r_obj, + nix_env = nix_env, + env_var = c(RETICULATE_AUTOCONFIGURE = "custom") + ) + expect_equal(res_override2$env_var[["RETICULATE_AUTOCONFIGURE"]], "custom") +}) + +test_that("env_var merging works correctly with other variables", { + nix_env <- "default.nix" + + res <- rxp_py2r( + name = my_obj, + expr = py_obj, + nix_env = nix_env, + env_var = c(OTHER_VAR = "foo") + ) + + expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") + expect_equal(res$env_var[["OTHER_VAR"]], "foo") +}) From bb57bf4e191d6ee07c62f5da4256575bb6d59ad2 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Wed, 4 Feb 2026 16:43:37 +0100 Subject: [PATCH 2/3] reticulate_config --- R/rxp_io.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/rxp_io.R b/R/rxp_io.R index 0fc4d94..99610f6 100644 --- a/R/rxp_io.R +++ b/R/rxp_io.R @@ -720,7 +720,7 @@ build_transfer_command <- function(out_name, expr_str, direction) { #' @noRd build_reticulate_phase <- function(r_command, env_exports = "") { sprintf( - "export RETICULATE_PYTHON=${defaultPkgs.python3}/bin/python\n %sRscript -e \"\n source('libraries.R')\n%s\"", + "# Clear any inherited PYTHONPATH\n unset PYTHONPATH || true\n # Dynamically determine Python version and set PYTHONPATH\n PYTHON_VERSION=$(${defaultPkgs.python3}/bin/python3 -c 'import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")')\n export PYTHONPATH=${defaultPkgs.python3}/lib/python$PYTHON_VERSION/site-packages\n export RETICULATE_PYTHON=${defaultPkgs.python3}/bin/python\n export RETICULATE_AUTOCONFIGURE=0\n %sRscript -e \"\n source('libraries.R')\n%s\"", env_exports, r_command ) From 44f6903f6c26da37ed930571b7d186fe36bfdacd Mon Sep 17 00:00:00 2001 From: b-rodrigues Date: Wed, 4 Feb 2026 15:46:22 +0000 Subject: [PATCH 3/3] Style via air --- R/rxp_io.R | 8 +++++++- R/rxp_make.R | 2 +- tests/testthat/test-reticulate-config.R | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/R/rxp_io.R b/R/rxp_io.R index 99610f6..49bb8aa 100644 --- a/R/rxp_io.R +++ b/R/rxp_io.R @@ -644,7 +644,13 @@ rxp_jl_file <- function(...) rxp_file("Jl", ...) #' @return A list with elements: `name`, `snippet`, `type`, `additional_files`, #' `nix_env`. #' @noRd -rxp_common_setup <- function(out_name, expr_str, nix_env, direction, env_var = NULL) { +rxp_common_setup <- function( + out_name, + expr_str, + nix_env, + direction, + env_var = NULL +) { expr_str <- gsub("\"", "'", expr_str) base <- sanitize_nix_env(nix_env) diff --git a/R/rxp_make.R b/R/rxp_make.R index ad75388..6901c48 100644 --- a/R/rxp_make.R +++ b/R/rxp_make.R @@ -506,4 +506,4 @@ rxp_import_artifacts <- function( message("Importing store paths from ", archive_file) system2("nix-store", args = "--import", stdin = archive_file) message("Import completed") -} \ No newline at end of file +} diff --git a/tests/testthat/test-reticulate-config.R b/tests/testthat/test-reticulate-config.R index 6eeae0f..233c087 100644 --- a/tests/testthat/test-reticulate-config.R +++ b/tests/testthat/test-reticulate-config.R @@ -1,37 +1,37 @@ test_that("rxp_py2r sets RETICULATE_AUTOCONFIGURE=0 by default", { # Mock user input nix_env <- "default.nix" - + # Default behavior res <- rxp_py2r( name = my_obj, expr = py_obj, nix_env = nix_env ) - + expect_true(!is.null(res$env_var)) expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") - + # Verify it appears in the snippet (indirectly validating build_reticulate_phase logic if it was simple string matching) # But better to check the structured 'env_var' component we added. }) test_that("rxp_r2py sets RETICULATE_AUTOCONFIGURE=0 by default", { nix_env <- "default.nix" - + res <- rxp_r2py( name = py_obj, expr = r_obj, nix_env = nix_env ) - + expect_true(!is.null(res$env_var)) expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") }) test_that("User can override RETICULATE_AUTOCONFIGURE", { nix_env <- "default.nix" - + # Override to "1" res_override <- rxp_py2r( name = my_obj, @@ -39,9 +39,9 @@ test_that("User can override RETICULATE_AUTOCONFIGURE", { nix_env = nix_env, env_var = c(RETICULATE_AUTOCONFIGURE = "1") ) - + expect_equal(res_override$env_var[["RETICULATE_AUTOCONFIGURE"]], "1") - + # Override to empty string/something else res_override2 <- rxp_r2py( name = py_obj, @@ -54,14 +54,14 @@ test_that("User can override RETICULATE_AUTOCONFIGURE", { test_that("env_var merging works correctly with other variables", { nix_env <- "default.nix" - + res <- rxp_py2r( name = my_obj, expr = py_obj, nix_env = nix_env, env_var = c(OTHER_VAR = "foo") ) - + expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") expect_equal(res$env_var[["OTHER_VAR"]], "foo") })