From 70027f9a7e7e429a23540283f133769660e627bc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 28 Apr 2023 11:24:28 +0100 Subject: [PATCH 01/47] Set dev version --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index e0513926c..75f214f32 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: sandpaper Title: Create and Curate Carpentries Lessons -Version: 0.11.15 +Version: 0.11.15.9000 Authors@R: c( person(given = "Zhian N.", family = "Kamvar", From 51c6995cb448e731dcb8f30e81a1d2e73dac416a Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 2 May 2023 17:15:50 +0100 Subject: [PATCH 02/47] Add `use_python()` helper --- NAMESPACE | 1 + R/use_python.R | 23 +++++++++++++++++++++++ man/use_python.Rd | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 R/use_python.R create mode 100644 man/use_python.Rd diff --git a/NAMESPACE b/NAMESPACE index fcf273ca2..8d936c8e6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -54,6 +54,7 @@ export(update_cache) export(update_github_workflows) export(update_varnish) export(use_package_cache) +export(use_python) export(validate_lesson) export(work_with_cache) importFrom(assertthat,validate_that) diff --git a/R/use_python.R b/R/use_python.R new file mode 100644 index 000000000..d81de0804 --- /dev/null +++ b/R/use_python.R @@ -0,0 +1,23 @@ +#' Use Python +#' +#' Associate a version of Python with your lesson. This is essentially a wrapper +#' around [renv::use_python()]. +#' +#' @param path path to the current project +#' @inheritParams renv::use_python +#' @param ... Further arguments to be passed on to [renv::use_python()] +#' +#' @export +#' @seealso [renv::use_python()] +#' @return The path to the Python executable. Note that this function is mainly +#' called for its side effects. +use_python <- function(path = ".", python = NULL, + type = c("auto", "virtualenv", "conda", "system"), ...) { + + renv::load(project = path) + on.exit({ + invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) + }, add = TRUE) + renv::use_python(python = python, type = type, ...) +} + diff --git a/man/use_python.Rd b/man/use_python.Rd new file mode 100644 index 000000000..f9c6aa881 --- /dev/null +++ b/man/use_python.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/use_python.R +\name{use_python} +\alias{use_python} +\title{Use Python} +\usage{ +use_python( + path = ".", + python = NULL, + type = c("auto", "virtualenv", "conda", "system"), + ... +) +} +\arguments{ +\item{path}{path to the current project} + +\item{python}{The path to the version of Python to be used with this project. See +\strong{Finding Python} for more details.} + +\item{type}{The type of Python environment to use. When \code{"auto"} (the default), +virtual environments will be used.} + +\item{...}{Further arguments to be passed on to \code{\link[renv:use_python]{renv::use_python()}}} +} +\value{ +The path to the Python executable. Note that this function is mainly +called for its side effects. +} +\description{ +Associate a version of Python with your lesson. This is essentially a wrapper +around \code{\link[renv:use_python]{renv::use_python()}}. +} +\seealso{ +\code{\link[renv:use_python]{renv::use_python()}} +} From 962339550de2af92376c87da07f111de2d98ca3d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 2 May 2023 18:26:52 +0100 Subject: [PATCH 03/47] Add `py_install()` helpeer to add Python dependencies --- DESCRIPTION | 3 ++- NAMESPACE | 1 + R/use_python.R | 43 +++++++++++++++++++++++++++++++++++++++++-- man/py_install.Rd | 27 +++++++++++++++++++++++++++ man/use_python.Rd | 2 +- 5 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 man/py_install.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 75f214f32..52a1bd295 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -60,7 +60,8 @@ Suggests: jsonlite, sessioninfo, mockr, - varnish (>= 0.2.1) + varnish (>= 0.2.1), + reticulate Additional_repositories: https://carpentries.r-universe.dev/ Remotes: ropensci/tinkr, diff --git a/NAMESPACE b/NAMESPACE index 8d936c8e6..c87e44f8a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,6 +24,7 @@ export(move_episode) export(no_package_cache) export(package_cache_trigger) export(pin_version) +export(py_install) export(renv_diagnostics) export(reset_episodes) export(reset_site) diff --git a/R/use_python.R b/R/use_python.R index d81de0804..d980c783a 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -1,7 +1,7 @@ #' Use Python #' #' Associate a version of Python with your lesson. This is essentially a wrapper -#' around [renv::use_python()]. +#' around [renv::use_python()]. To add Python packages, use [py_install()]. #' #' @param path path to the current project #' @inheritParams renv::use_python @@ -14,10 +14,49 @@ use_python <- function(path = ".", python = NULL, type = c("auto", "virtualenv", "conda", "system"), ...) { - renv::load(project = path) + ## Load the renv profile, unloading it upon exit on.exit({ invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) }, add = TRUE) + renv::load(project = path) + renv::use_python(python = python, type = type, ...) } + +#' Install Python packages and add them to the cache +#' +#' Installs Python packages with [reticulate::py_install()] and then records +#' them in the renv environment. This ensures [manage_deps()] keeps track of the +#' Python packages as well. +#' +#' @param packages Python packages to be installed as a character vecto. +#' @param path path to your lesson. Defaults to the current working directory. +#' @param ... Further arguments to be passed to [reticulate::py_install()] +#' +#' @details +#' Unlike with R packages, \pkg{renv} is not yet capable of automatically +#' detecting Python dependencies. Therefore, this helper function is provided to +#' correctly install Python packages and recording them in the renv environment. +#' Subsequent calls of [manage_deps()] will then correctly restore the required +#' Python packages if needed. +#' +#' @export +py_install <- function(packages, path = ".", ...) { + + ## Load the renv profile, unloading it upon exit + renv::load(project = path) + on.exit({ + invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) + }, add = TRUE) + + has_reticulate <- rlang::is_installed("reticulate") + if (!has_reticulate) { + cli::cli_alert("Adding `reticulate` as a dependency for Python package installation") + renv::install("reticulate") + } + reticulate::py_install(packages = packages, ...) + + cli::cli_alert("Updating the package cache") + renv::snapshot(lockfile = renv::paths$lockfile(project = path), prompt = FALSE) +} diff --git a/man/py_install.Rd b/man/py_install.Rd new file mode 100644 index 000000000..945645217 --- /dev/null +++ b/man/py_install.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/use_python.R +\name{py_install} +\alias{py_install} +\title{Install Python packages and add them to the cache} +\usage{ +py_install(path = ".", packages, ...) +} +\arguments{ +\item{path}{path to your lesson. Defaults to the current working directory.} + +\item{packages}{Python packages to be installed as a character vecto.} + +\item{...}{Further arguments to be passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}} +} +\description{ +Installs Python packages with \code{\link[reticulate:py_install]{reticulate::py_install()}} and then records +them in the renv environment. This ensures \code{\link[=manage_deps]{manage_deps()}} keeps track of the +Python packages as well. +} +\details{ +Unlike with R packages, \pkg{renv} is not yet capable of automatically +detecting Python dependencies. Therefore, this helper function is provided to +correctly install Python packages and recording them in the renv environment. +Subsequent calls of \code{\link[=manage_deps]{manage_deps()}} will then correctly restore the required +Python packages if needed. +} diff --git a/man/use_python.Rd b/man/use_python.Rd index f9c6aa881..ff3a9a53e 100644 --- a/man/use_python.Rd +++ b/man/use_python.Rd @@ -28,7 +28,7 @@ called for its side effects. } \description{ Associate a version of Python with your lesson. This is essentially a wrapper -around \code{\link[renv:use_python]{renv::use_python()}}. +around \code{\link[renv:use_python]{renv::use_python()}}. To add Python packages, use \code{\link[=py_install]{py_install()}}. } \seealso{ \code{\link[renv:use_python]{renv::use_python()}} From 1495f893acf083b5bc615bcb7084d03f0e8f14d8 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 11:47:40 +0100 Subject: [PATCH 04/47] Reactivate renv profile after adding Python Workaround for https://github.com/rstudio/renv/issues/1217 --- R/use_python.R | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/R/use_python.R b/R/use_python.R index d980c783a..6258af922 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -18,9 +18,14 @@ use_python <- function(path = ".", python = NULL, on.exit({ invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) }, add = TRUE) - renv::load(project = path) + renv::load(project = path) + prof <- Sys.getenv("RENV_PROFILE") renv::use_python(python = python, type = type, ...) + + ## NOTE: use_python() deactivates the default profile, see https://github.com/rstudio/renv/issues/1217 + ## Workaround: re-activate the profile + renv::activate(project = path, profile = prof) } From cea48bfc5eed7f5a8875a9ef620e79edecc3def3 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 11:57:08 +0100 Subject: [PATCH 05/47] Make sure Python dependencies also get restored by `manage_deps()` --- R/utils-renv.R | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/R/utils-renv.R b/R/utils-renv.R index 7bd35a5e2..3d201b96e 100644 --- a/R/utils-renv.R +++ b/R/utils-renv.R @@ -288,8 +288,12 @@ callr_manage_deps <- function(path, repos, snapshot, lockfile_exists) { # recorded. if (lockfile_exists) { cli::cli_alert("Restoring any dependency versions") - res <- renv::restore(project = path, library = renv_lib, - lockfile = renv_lock, prompt = FALSE) + # Load profile, this ensures Python dependencies also get restored + renv::load(project = path) + on.exit({ + invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) + }, add = TRUE) + res <- renv::restore(project = path, prompt = FALSE) } if (snapshot) { # 3. Load the current profile, unloading it when we exit From 28425e8cae083933d1914d8da9949497a33f726b Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 13:27:54 +0100 Subject: [PATCH 06/47] Add option to set up Python in `create_lesson()` --- R/create_lesson.R | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/R/create_lesson.R b/R/create_lesson.R index 5c91ba819..456296036 100644 --- a/R/create_lesson.R +++ b/R/create_lesson.R @@ -19,7 +19,8 @@ #' on.exit(unlink(tmp)) #' lsn <- create_lesson(tmp, name = "This Lesson", open = FALSE) #' lsn -create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio = rstudioapi::isAvailable(), open = rlang::is_interactive()) { +create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio = rstudioapi::isAvailable(), open = rlang::is_interactive(), + add_python = FALSE, python = NULL, type = c("auto", "virtualenv", "conda", "system")) { path <- fs::path_abs(path) id <- cli::cli_status("{cli::symbol$arrow_right} Creating Lesson in {.file {path}}...") @@ -92,6 +93,11 @@ create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio if (has_consent) { cli::cli_status_update("{cli::symbol$arrow_right} Managing Dependencies ...") manage_deps(path, snapshot = TRUE) + + if (add_python) { + cli::cli_status_update("{cli::symbol$arrow_right} Setting up Python ...") + use_python(path = path, python = python, type = type) + } } cli::cli_status_update("{cli::symbol$arrow_right} Committing ...") From b729c0ca33b3823104dd3f2dfd04c35a6647182b Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 13:28:20 +0100 Subject: [PATCH 07/47] Set working directory while setting up Python Avoids some undesirable renv side effects. --- R/use_python.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/R/use_python.R b/R/use_python.R index 6258af922..2d498d54c 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -14,13 +14,19 @@ use_python <- function(path = ".", python = NULL, type = c("auto", "virtualenv", "conda", "system"), ...) { + wd <- getwd() + ## Load the renv profile, unloading it upon exit on.exit({ invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) + setwd(wd) }, add = TRUE) + ## Set up working directory, avoids some renv side effects + setwd(path) renv::load(project = path) prof <- Sys.getenv("RENV_PROFILE") + renv::use_python(python = python, type = type, ...) ## NOTE: use_python() deactivates the default profile, see https://github.com/rstudio/renv/issues/1217 From 5ffacc6e7fbeb5d9a32d537e6770ca17bacc1c07 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 13:43:01 +0100 Subject: [PATCH 08/47] Update documentation for `create_lesson()` --- R/create_lesson.R | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/R/create_lesson.R b/R/create_lesson.R index 456296036..963e537da 100644 --- a/R/create_lesson.R +++ b/R/create_lesson.R @@ -11,6 +11,16 @@ #' file extension in the lesson. #' @param rstudio create an RStudio project (defaults to if RStudio exits) #' @param open if interactive, the lesson will open in a new editor window. +#' @param add_python if set to `TRUE`, will add Python as a dependency for the +#' lesson. See [use_python()] for details. Defaults to `FALSE`. +#' @param python the path to the version of Python to be used. The default, +#' `NULL`, will prompt the user to select an appropriate version of Python in +#' interactive sessions. In non-interactive sessions, \pkg{renv} will attempt +#' to automatically select an appropriate version. See [renv::use_python()] +#' for more details. +#' @param type the type of Python environment to use. When `"auto"`, the +#' default, virtual environments will be used. See [renv::use_python()] for +#' more details. #' #' @export #' @return the path to the new lesson From 669141ce122621ed108d8af46439bd17eec2f9e8 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 13:53:51 +0100 Subject: [PATCH 09/47] Update documentation for `use_python()` --- R/use_python.R | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/R/use_python.R b/R/use_python.R index 2d498d54c..67ad93e13 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -1,14 +1,29 @@ -#' Use Python +#' Add Python as a lesson dependency #' #' Associate a version of Python with your lesson. This is essentially a wrapper -#' around [renv::use_python()]. To add Python packages, use [py_install()]. +#' around [renv::use_python()]. #' #' @param path path to the current project #' @inheritParams renv::use_python #' @param ... Further arguments to be passed on to [renv::use_python()] #' +#' @details +#' This helper function adds Python as a dependency to the \pkg{renv} lockfile +#' and installs a Python environment of the specified `type`. This ensures any +#' Python packages used for this lesson are installed separately from the user's +#' main library, much like the R packages (see [manage_deps()]). +#' +#' Note that \pkg{renv} is not (yet) able to automatically detect Python package +#' dependencies (e.g. from `import` statements). So any required Python packages +#' still need to be installed manually. To facilitate this, the [py_install()] +#' helper is provided. This will install Python packages in the correct +#' environment and record them in a `requirements.txt` file, which will be +#' tracked by \pkg{renv}. Subsequent calls of [manage_deps()] will then +#' correctly restore the required Python packages if needed. +#' #' @export -#' @seealso [renv::use_python()] +#' @rdname use_python +#' @seealso [renv::use_python()], [py_install()] #' @return The path to the Python executable. Note that this function is mainly #' called for its side effects. use_python <- function(path = ".", python = NULL, @@ -37,22 +52,17 @@ use_python <- function(path = ".", python = NULL, #' Install Python packages and add them to the cache #' -#' Installs Python packages with [reticulate::py_install()] and then records -#' them in the renv environment. This ensures [manage_deps()] keeps track of the -#' Python packages as well. +#' To add Python packages, `py_install()` is provided, which installs Python +#' packages with [reticulate::py_install()] and then records them in the renv +#' environment. This ensures [manage_deps()] keeps track of the Python packages +#' as well. #' #' @param packages Python packages to be installed as a character vecto. #' @param path path to your lesson. Defaults to the current working directory. #' @param ... Further arguments to be passed to [reticulate::py_install()] #' -#' @details -#' Unlike with R packages, \pkg{renv} is not yet capable of automatically -#' detecting Python dependencies. Therefore, this helper function is provided to -#' correctly install Python packages and recording them in the renv environment. -#' Subsequent calls of [manage_deps()] will then correctly restore the required -#' Python packages if needed. -#' #' @export +#' @rdname use_python py_install <- function(packages, path = ".", ...) { ## Load the renv profile, unloading it upon exit From 0b45377ebc8e6e215a00f1422150427e56015c92 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 14:01:56 +0100 Subject: [PATCH 10/47] Reoxygenize --- man/create_lesson.Rd | 18 +++++++++++++++++- man/py_install.Rd | 27 --------------------------- man/use_python.Rd | 34 +++++++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 33 deletions(-) delete mode 100644 man/py_install.Rd diff --git a/man/create_lesson.Rd b/man/create_lesson.Rd index 2afaa0187..ca65dbd9d 100644 --- a/man/create_lesson.Rd +++ b/man/create_lesson.Rd @@ -9,7 +9,10 @@ create_lesson( name = fs::path_file(path), rmd = TRUE, rstudio = rstudioapi::isAvailable(), - open = rlang::is_interactive() + open = rlang::is_interactive(), + add_python = FALSE, + python = NULL, + type = c("auto", "virtualenv", "conda", "system") ) } \arguments{ @@ -25,6 +28,19 @@ file extension in the lesson.} \item{rstudio}{create an RStudio project (defaults to if RStudio exits)} \item{open}{if interactive, the lesson will open in a new editor window.} + +\item{add_python}{if set to \code{TRUE}, will add Python as a dependency for the +lesson. See \code{\link[=use_python]{use_python()}} for details. Defaults to \code{FALSE}.} + +\item{python}{the path to the version of Python to be used. The default, +\code{NULL}, will prompt the user to select an appropriate version of Python in +interactive sessions. In non-interactive sessions, \pkg{renv} will attempt +to automatically select an appropriate version. See \code{\link[renv:use_python]{renv::use_python()}} +for more details.} + +\item{type}{the type of Python environment to use. When \code{"auto"}, the +default, virtual environments will be used. See \code{\link[renv:use_python]{renv::use_python()}} for +more details.} } \value{ the path to the new lesson diff --git a/man/py_install.Rd b/man/py_install.Rd deleted file mode 100644 index 945645217..000000000 --- a/man/py_install.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/use_python.R -\name{py_install} -\alias{py_install} -\title{Install Python packages and add them to the cache} -\usage{ -py_install(path = ".", packages, ...) -} -\arguments{ -\item{path}{path to your lesson. Defaults to the current working directory.} - -\item{packages}{Python packages to be installed as a character vecto.} - -\item{...}{Further arguments to be passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}} -} -\description{ -Installs Python packages with \code{\link[reticulate:py_install]{reticulate::py_install()}} and then records -them in the renv environment. This ensures \code{\link[=manage_deps]{manage_deps()}} keeps track of the -Python packages as well. -} -\details{ -Unlike with R packages, \pkg{renv} is not yet capable of automatically -detecting Python dependencies. Therefore, this helper function is provided to -correctly install Python packages and recording them in the renv environment. -Subsequent calls of \code{\link[=manage_deps]{manage_deps()}} will then correctly restore the required -Python packages if needed. -} diff --git a/man/use_python.Rd b/man/use_python.Rd index ff3a9a53e..7334d1a72 100644 --- a/man/use_python.Rd +++ b/man/use_python.Rd @@ -2,7 +2,8 @@ % Please edit documentation in R/use_python.R \name{use_python} \alias{use_python} -\title{Use Python} +\alias{py_install} +\title{Add Python as a lesson dependency} \usage{ use_python( path = ".", @@ -10,9 +11,11 @@ use_python( type = c("auto", "virtualenv", "conda", "system"), ... ) + +py_install(packages, path = ".", ...) } \arguments{ -\item{path}{path to the current project} +\item{path}{path to your lesson. Defaults to the current working directory.} \item{python}{The path to the version of Python to be used with this project. See \strong{Finding Python} for more details.} @@ -20,7 +23,9 @@ use_python( \item{type}{The type of Python environment to use. When \code{"auto"} (the default), virtual environments will be used.} -\item{...}{Further arguments to be passed on to \code{\link[renv:use_python]{renv::use_python()}}} +\item{...}{Further arguments to be passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}} + +\item{packages}{Python packages to be installed as a character vecto.} } \value{ The path to the Python executable. Note that this function is mainly @@ -28,8 +33,27 @@ called for its side effects. } \description{ Associate a version of Python with your lesson. This is essentially a wrapper -around \code{\link[renv:use_python]{renv::use_python()}}. To add Python packages, use \code{\link[=py_install]{py_install()}}. +around \code{\link[renv:use_python]{renv::use_python()}}. + +To add Python packages, \code{py_install()} is provided, which installs Python +packages with \code{\link[reticulate:py_install]{reticulate::py_install()}} and then records them in the renv +environment. This ensures \code{\link[=manage_deps]{manage_deps()}} keeps track of the Python packages +as well. +} +\details{ +This helper function adds Python as a dependency to the \pkg{renv} lockfile +and installs a Python environment of the specified \code{type}. This ensures any +Python packages used for this lesson are installed separately from the user's +main library, much like the R packages (see \code{\link[=manage_deps]{manage_deps()}}). + +Note that \pkg{renv} is not (yet) able to automatically detect Python package +dependencies (e.g. from \code{import} statements). So any required Python packages +still need to be installed manually. To facilitate this, the \code{\link[=py_install]{py_install()}} +helper is provided. This will install Python packages in the correct +environment and record them in a \code{requirements.txt} file, which will be +tracked by \pkg{renv}. Subsequent calls of \code{\link[=manage_deps]{manage_deps()}} will then +correctly restore the required Python packages if needed. } \seealso{ -\code{\link[renv:use_python]{renv::use_python()}} +\code{\link[renv:use_python]{renv::use_python()}}, \code{\link[=py_install]{py_install()}} } From 4529311daa87f2cf6085e20c92699efe835ea243 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 14:13:46 +0100 Subject: [PATCH 11/47] Add examples for Python usage --- R/use_python.R | 16 ++++++++++++++++ man/use_python.Rd | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/R/use_python.R b/R/use_python.R index 67ad93e13..a9b6e33b9 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -26,6 +26,22 @@ #' @seealso [renv::use_python()], [py_install()] #' @return The path to the Python executable. Note that this function is mainly #' called for its side effects. +#' @examples +#' \dontrun{ +#' tmp <- tempfile() +#' on.exit(unlink(tmp)) +#' +#' ## Create lesson with Python support +#' lsn <- create_lesson(tmp, name = "This Lesson", open = FALSE, add_python = TRUE) +#' lsn +#' +#' ## Add Python as a dependency to an existing lesson +#' setwd(lsn) +#' use_python() +#' +#' ## Install Python packages and record them as dependencies +#' py_install("numpy") +#' } use_python <- function(path = ".", python = NULL, type = c("auto", "virtualenv", "conda", "system"), ...) { diff --git a/man/use_python.Rd b/man/use_python.Rd index 7334d1a72..f6ce78715 100644 --- a/man/use_python.Rd +++ b/man/use_python.Rd @@ -54,6 +54,23 @@ environment and record them in a \code{requirements.txt} file, which will be tracked by \pkg{renv}. Subsequent calls of \code{\link[=manage_deps]{manage_deps()}} will then correctly restore the required Python packages if needed. } +\examples{ +\dontrun{ +tmp <- tempfile() +on.exit(unlink(tmp)) + +## Create lesson with Python support +lsn <- create_lesson(tmp, name = "This Lesson", open = FALSE, add_python = TRUE) +lsn + +## Add Python as a dependency to an existing lesson +setwd(lsn) +use_python() + +## Install Python packages and record them as dependencies +py_install("numpy") +} +} \seealso{ \code{\link[renv:use_python]{renv::use_python()}}, \code{\link[=py_install]{py_install()}} } From ec2599f9967ca4dc9f880a6bba5436c480b9f7bf Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 16:18:36 +0100 Subject: [PATCH 12/47] Add unit tests for Python integration --- tests/testthat/test-use_python.R | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/testthat/test-use_python.R diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R new file mode 100644 index 000000000..d49e599f4 --- /dev/null +++ b/tests/testthat/test-use_python.R @@ -0,0 +1,36 @@ +lsn <- restore_fixture() +old_wd <- setwd(lsn) +withr::defer(setwd(old_wd)) + +use_python(lsn, type = "virtualenv") + + +test_that("use_python() adds Python environment", { + py_path <- fs::path(lsn, "renv/profiles/lesson-requirements/renv/python") + + expect_true(fs::dir_exists(py_path)) + expect_false(Sys.getenv("RETICULATE_PYTHON") == "") + py_config <- reticulate::py_config() + expect_true(py_config$available) + expect_true(py_config$forced == "RETICULATE_PYTHON") +}) + +## This relates to a bug in renv, see https://github.com/rstudio/renv/issues/1217 +test_that("use_python() does not remove renv/profile", { + expect_true(fs::file_exists(fs::path(lsn, "renv/profile"))) +}) + + +test_that("py_install() installs Python packages", { + py_install("numpy", path = lsn) + + expect_no_error({numpy <- reticulate::import("numpy")}) + expect_s3_class(numpy, "python.builtin.module") +}) + + +test_that("py_install() updates requirements.txt", { + py_install("numpy", path = lsn) + req_file <- fs::path(lsn, "requirements.txt") + expect_true(grepl("^numpy", readLines(req_file))) +}) From cc05834c0d4ad2fbaa3bd8fb8bea38e6df3e5ea5 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 3 May 2023 17:19:27 +0100 Subject: [PATCH 13/47] Also test that `manage_deps()` plays nice with Python integration --- tests/testthat/test-manage_deps.R | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index eae7f41c5..2416b6d18 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -207,3 +207,31 @@ test_that("update_cache() will update old package versions", { }) + +test_that("manage_deps() does not overwrite requirements.txt", { + old_wd <- setwd(lsn) + withr::defer(setwd(old_wd)) + + ## Set up Python and manually add requirements.txt without actually installing + ## the Python package, mimicking the scenario where a Python dependency is missing + use_python(lsn, type = "virtualenv") + req_file <- fs::path(lsn, "requirements.txt") + writeLines("numpy", req_file) + + res <- manage_deps(lsn, quiet = TRUE) + expect_true(grepl("^numpy", readLines(req_file))) +}) + + +test_that("manage_deps() restores Python dependencies", { + old_wd <- setwd(lsn) + withr::defer(setwd(old_wd)) + use_python(lsn, type = "virtualenv") + + req_file <- fs::path(lsn, "requirements.txt") + writeLines("numpy", req_file) + res <- manage_deps(lsn, quiet = TRUE) + + expect_no_error({numpy <- reticulate::import("numpy")}) + expect_s3_class(numpy, "python.builtin.module") +}) From 1dd787a98e54e69f2aa9c5c732d61d4abe1b90cc Mon Sep 17 00:00:00 2001 From: zkamvar Date: Wed, 3 May 2023 17:31:47 +0000 Subject: [PATCH 14/47] Document --- man/build_episode_html.Rd | 6 +++--- man/build_episode_md.Rd | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/man/build_episode_html.Rd b/man/build_episode_html.Rd index 90a2df67f..978bb7c8e 100644 --- a/man/build_episode_html.Rd +++ b/man/build_episode_html.Rd @@ -57,7 +57,7 @@ they are doing. if (FALSE) { # 2022-04-15: this suddenly started throwing a check error # that says "connections left open: (file) and I can't figure out where the -# hell its coming from, so I'm just going to not run this :( +# hell its coming from, so I'm just going to not run this :( if (.Platform$OS.type == "windows") { options("sandpaper.use_renv" = FALSE) } @@ -78,7 +78,7 @@ if (rmarkdown::pandoc_available("2.11")) { fun_file <- file.path(tmp, "episodes", "files", "fun.Rmd") txt <- c( "---\ntitle: Fun times\n---\n\n", - "# new page\n", + "# new page\n", "This is coming from `r R.version.string`\n", "::: testimonial\n\n#### testimony!\n\nwhat\n:::\n" ) @@ -92,7 +92,7 @@ if (rmarkdown::pandoc_available("2.11")) { sandpaper:::set_globals(res) on.exit(clear_globals(), add = TRUE) # we can only build this if we have pandoc - build_episode_html(res, path_src = fun_file, + build_episode_html(res, path_src = fun_file, pkg = pkgdown::as_pkgdown(file.path(tmp, "site")) ) } diff --git a/man/build_episode_md.Rd b/man/build_episode_md.Rd index e34d3972f..f41af81a8 100644 --- a/man/build_episode_md.Rd +++ b/man/build_episode_md.Rd @@ -62,7 +62,7 @@ fun_file <- file.path(fun_dir, "episodes", "fun.Rmd") file.create(fun_file) txt <- c( "---\ntitle: Fun times\n---\n\n", - "# new page\n", + "# new page\n", "This is coming from `r R.version.string`" ) writeLines(txt, fun_file) From df07005341aa3e569984625373e4fe1119f4ec60 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 22 May 2023 11:35:53 +0100 Subject: [PATCH 15/47] Refactor: move reticulate installation to separate helper --- R/use_python.R | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/R/use_python.R b/R/use_python.R index a9b6e33b9..238d46dc4 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -51,18 +51,20 @@ use_python <- function(path = ".", python = NULL, on.exit({ invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) setwd(wd) - }, add = TRUE) + }, add = TRUE, after = FALSE) ## Set up working directory, avoids some renv side effects setwd(path) renv::load(project = path) prof <- Sys.getenv("RENV_PROFILE") + install_reticulate(path = path) renv::use_python(python = python, type = type, ...) ## NOTE: use_python() deactivates the default profile, see https://github.com/rstudio/renv/issues/1217 ## Workaround: re-activate the profile renv::activate(project = path, profile = prof) + invisible(path) } @@ -83,17 +85,27 @@ py_install <- function(packages, path = ".", ...) { ## Load the renv profile, unloading it upon exit renv::load(project = path) + on.exit({ invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) - }, add = TRUE) + }, add = TRUE, after = FALSE) - has_reticulate <- rlang::is_installed("reticulate") - if (!has_reticulate) { - cli::cli_alert("Adding `reticulate` as a dependency for Python package installation") - renv::install("reticulate") - } + install_reticulate(path = path) reticulate::py_install(packages = packages, ...) cli::cli_alert("Updating the package cache") renv::snapshot(lockfile = renv::paths$lockfile(project = path), prompt = FALSE) } + +install_reticulate <- function(path) { + renv_lib <- renv::paths$library(project = path) + has_reticulate <- require("reticulate", lib.loc = renv_lib, quietly = TRUE, character.only = TRUE) + if (!has_reticulate) { + cli::cli_alert("Adding `reticulate` as a dependency") + ## Force reticulate to be recorded by renv + dep_file <- fs::path(path, "dependencies.R") + write("library(reticulate)", file = dep_file, append = TRUE) + renv::install("reticulate", library = renv_lib) + } + invisible(NULL) +} From 7deb07df3997e770ab6d39b7eeb875a0a7a706b5 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 22 May 2023 11:36:52 +0100 Subject: [PATCH 16/47] Improve tests, add helpers to manage renv during testing --- tests/testthat/helper-python.R | 31 +++++++++++++++++++++++++++++++ tests/testthat/test-use_python.R | 29 ++++++++++++++++------------- 2 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 tests/testthat/helper-python.R diff --git a/tests/testthat/helper-python.R b/tests/testthat/helper-python.R new file mode 100644 index 000000000..c75f50d59 --- /dev/null +++ b/tests/testthat/helper-python.R @@ -0,0 +1,31 @@ +## Helpers to temporarily load renv environment +local_load_py_pkg <- function(lsn, package) { + local_renv_load(lsn) + reticulate::import(package) +} + +## These helpers are used in `test-use_python.R`, they are implemented separately to ensure the +## temporary loading of the renv environment doesn't interfere with the testing environment +check_reticulate <- function(lsn) { + local_renv_load(lsn) + lib <- renv::paths$library(project = lsn) + withr::local_libpaths(lib) + rlang::is_installed("reticulate") +} + +check_reticulate_config <- function(lsn) { + local_renv_load(lsn) + reticulate::py_config() +} + +get_renv_env <- function(lsn, which = "RETICULATE_PYTHON") { + local_renv_load(lsn) + Sys.getenv(which) +} + +## Temporarily load a renv profile, unloading it upon exit +local_renv_load <- function(lsn, env = parent.frame()) { + ## NOTE: renv:::unload() is currently not exported: https://github.com/rstudio/renv/issues/1285 + withr::defer(renv:::unload(project = lsn), envir = env) + renv::load(lsn) +} diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R index d49e599f4..3f5cf79eb 100644 --- a/tests/testthat/test-use_python.R +++ b/tests/testthat/test-use_python.R @@ -1,36 +1,39 @@ +## Set up temporary lesson with Python installed, for use in all subsequent tests lsn <- restore_fixture() -old_wd <- setwd(lsn) -withr::defer(setwd(old_wd)) - -use_python(lsn, type = "virtualenv") - +lsn <- use_python(lsn) test_that("use_python() adds Python environment", { py_path <- fs::path(lsn, "renv/profiles/lesson-requirements/renv/python") - expect_true(fs::dir_exists(py_path)) - expect_false(Sys.getenv("RETICULATE_PYTHON") == "") - py_config <- reticulate::py_config() +}) + +test_that("reticulate is installed", { + has_reticulate <- check_reticulate(lsn) + expect_true(has_reticulate) +}) + +test_that("use_python() sets reticulate configuration", { + reticulate_python_env <- get_renv_env(lsn, "RETICULATE_PYTHON") + py_config <- check_reticulate_config(lsn) + + expect_false(reticulate_python_env == "") expect_true(py_config$available) expect_true(py_config$forced == "RETICULATE_PYTHON") }) + ## This relates to a bug in renv, see https://github.com/rstudio/renv/issues/1217 test_that("use_python() does not remove renv/profile", { expect_true(fs::file_exists(fs::path(lsn, "renv/profile"))) }) - test_that("py_install() installs Python packages", { py_install("numpy", path = lsn) + numpy <- local_load_py_pkg(lsn, "numpy") expect_no_error({numpy <- reticulate::import("numpy")}) expect_s3_class(numpy, "python.builtin.module") -}) - -test_that("py_install() updates requirements.txt", { - py_install("numpy", path = lsn) req_file <- fs::path(lsn, "requirements.txt") expect_true(grepl("^numpy", readLines(req_file))) }) From ac15e6c78a207dbc89d1ba0707c8db34f3daf917 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 22 May 2023 11:47:27 +0100 Subject: [PATCH 17/47] Use `requireNamespace()` instead of `require()` --- R/use_python.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/use_python.R b/R/use_python.R index 238d46dc4..cadc51ea1 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -99,7 +99,7 @@ py_install <- function(packages, path = ".", ...) { install_reticulate <- function(path) { renv_lib <- renv::paths$library(project = path) - has_reticulate <- require("reticulate", lib.loc = renv_lib, quietly = TRUE, character.only = TRUE) + has_reticulate <- requireNamespace("reticulate", lib.loc = renv_lib, quietly = TRUE) if (!has_reticulate) { cli::cli_alert("Adding `reticulate` as a dependency") ## Force reticulate to be recorded by renv From 2df6d49fa971a47d45ba30081997e3ce07abbc35 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 22 May 2023 11:59:35 +0100 Subject: [PATCH 18/47] Set dev version --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 4cd8bec7b..729e415c1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: sandpaper Title: Create and Curate Carpentries Lessons -Version: 0.12.0 +Version: 0.12.0.9000 Authors@R: c( person(given = "Zhian N.", family = "Kamvar", From d096467966b4a8c1674b9e3d892eed405e18e2ca Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 22 May 2023 12:25:39 +0100 Subject: [PATCH 19/47] Skip `manage_deps()` tests on Windows --- tests/testthat/test-manage_deps.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 1f3cbe21c..0f6bf0b99 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -216,6 +216,9 @@ test_that("update_cache() will update old package versions", { test_that("manage_deps() does not overwrite requirements.txt", { + skip_on_cran() + skip_on_os("windows") + old_wd <- setwd(lsn) withr::defer(setwd(old_wd)) @@ -231,6 +234,9 @@ test_that("manage_deps() does not overwrite requirements.txt", { test_that("manage_deps() restores Python dependencies", { + skip_on_cran() + skip_on_os("windows") + old_wd <- setwd(lsn) withr::defer(setwd(old_wd)) use_python(lsn, type = "virtualenv") From fde134bbe2d1eaef0d368ebd3201ef38a49b531b Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 7 Jun 2023 11:30:16 +0100 Subject: [PATCH 20/47] Skip `use_python` tests on Windows Not sure why they are not working. Assuming it has to do with renv. --- tests/testthat/test-use_python.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R index 3f5cf79eb..81261bdcb 100644 --- a/tests/testthat/test-use_python.R +++ b/tests/testthat/test-use_python.R @@ -3,6 +3,7 @@ lsn <- restore_fixture() lsn <- use_python(lsn) test_that("use_python() adds Python environment", { + skip_on_os("windows") py_path <- fs::path(lsn, "renv/profiles/lesson-requirements/renv/python") expect_true(fs::dir_exists(py_path)) }) @@ -13,6 +14,7 @@ test_that("reticulate is installed", { }) test_that("use_python() sets reticulate configuration", { + skip_on_os("windows") reticulate_python_env <- get_renv_env(lsn, "RETICULATE_PYTHON") py_config <- check_reticulate_config(lsn) @@ -24,10 +26,12 @@ test_that("use_python() sets reticulate configuration", { ## This relates to a bug in renv, see https://github.com/rstudio/renv/issues/1217 test_that("use_python() does not remove renv/profile", { + skip_on_os("windows") expect_true(fs::file_exists(fs::path(lsn, "renv/profile"))) }) test_that("py_install() installs Python packages", { + skip_on_os("windows") py_install("numpy", path = lsn) numpy <- local_load_py_pkg(lsn, "numpy") From 680c74e9ba1b5de309bd15e1ada3e9d38c10786c Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 8 Jun 2023 16:15:08 +0100 Subject: [PATCH 21/47] Check that reticulate can be installed and warn if it does not --- R/use_python.R | 15 ++++++++++++++- tests/testthat/test-manage_deps.R | 3 +++ tests/testthat/test-use_python.R | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/R/use_python.R b/R/use_python.R index cadc51ea1..3f5dcf648 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -100,7 +100,8 @@ py_install <- function(packages, path = ".", ...) { install_reticulate <- function(path) { renv_lib <- renv::paths$library(project = path) has_reticulate <- requireNamespace("reticulate", lib.loc = renv_lib, quietly = TRUE) - if (!has_reticulate) { + reticulate_installable <- check_reticulate_installable() + if (!has_reticulate && reticulate_installable) { cli::cli_alert("Adding `reticulate` as a dependency") ## Force reticulate to be recorded by renv dep_file <- fs::path(path, "dependencies.R") @@ -109,3 +110,15 @@ install_reticulate <- function(path) { } invisible(NULL) } + +check_reticulate_installable <- function() { + r_compatible <- is_r_version_greater_than(minimal_major = 4) + if (!r_compatible) { + cli::cli_warn("R version {minimal_major}.0 or higher is required for reticulate") + } + r_compatible +} + +is_r_version_greater_than <- function(minimal_major = 4) { + R.version$major >= minimal_major +} diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 0f6bf0b99..d1f74f4a3 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -214,8 +214,10 @@ test_that("update_cache() will update old package versions", { }) +reticulate_installable <- check_reticulate_installable() test_that("manage_deps() does not overwrite requirements.txt", { + skip_if_not(reticulate_installable, "reticulate is not installable") skip_on_cran() skip_on_os("windows") @@ -234,6 +236,7 @@ test_that("manage_deps() does not overwrite requirements.txt", { test_that("manage_deps() restores Python dependencies", { + skip_if_not(reticulate_installable, "reticulate is not installable") skip_on_cran() skip_on_os("windows") diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R index 81261bdcb..a5940b912 100644 --- a/tests/testthat/test-use_python.R +++ b/tests/testthat/test-use_python.R @@ -2,6 +2,10 @@ lsn <- restore_fixture() lsn <- use_python(lsn) +suppressWarnings({ + reticulate_installable <- check_reticulate_installable() +}) + test_that("use_python() adds Python environment", { skip_on_os("windows") py_path <- fs::path(lsn, "renv/profiles/lesson-requirements/renv/python") @@ -9,10 +13,18 @@ test_that("use_python() adds Python environment", { }) test_that("reticulate is installed", { + skip_if_not(reticulate_installable, "reticulate is not installable") has_reticulate <- check_reticulate(lsn) expect_true(has_reticulate) }) +test_that("A warning is generated when reticulate is not installable", { + skip_if(reticulate_installable, "reticulate is installable") + expect_warning(install_reticulate(lsn)) + has_reticulate <- check_reticulate(lsn) + expect_false(has_reticulate) +}) + test_that("use_python() sets reticulate configuration", { skip_on_os("windows") reticulate_python_env <- get_renv_env(lsn, "RETICULATE_PYTHON") @@ -31,7 +43,9 @@ test_that("use_python() does not remove renv/profile", { }) test_that("py_install() installs Python packages", { + skip_if_not(reticulate_installable, "reticulate is not installable") skip_on_os("windows") + py_install("numpy", path = lsn) numpy <- local_load_py_pkg(lsn, "numpy") From da24671e40431f31eda1ec816046125ae5d8c17f Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 8 Jun 2023 17:31:18 +0100 Subject: [PATCH 22/47] Small bug fix --- R/use_python.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/use_python.R b/R/use_python.R index 3f5dcf648..889108639 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -112,7 +112,8 @@ install_reticulate <- function(path) { } check_reticulate_installable <- function() { - r_compatible <- is_r_version_greater_than(minimal_major = 4) + minimal_major <- 4 + r_compatible <- is_r_version_greater_than(minimal_major = minimal_major) if (!r_compatible) { cli::cli_warn("R version {minimal_major}.0 or higher is required for reticulate") } From a589e1c522918af907670b7a9e21b90386424624 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 14 Jun 2023 09:16:11 +0100 Subject: [PATCH 23/47] Add `use_python` to pkgdown config --- _pkgdown.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/_pkgdown.yml b/_pkgdown.yml index 104d19518..6088974c3 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -18,7 +18,7 @@ code: reference: - title: "Lesson Creation" desc: > - Provision new lessons and/or episodes. These functions will likely only + Provision new lessons and/or episodes. These functions will likely only be used once. - contents: - create_lesson @@ -35,7 +35,7 @@ reference: - title: "Lesson Development Helpers" desc: > Functions to programmatically assess and modify configuration and source - elements of a lesson. These are often used when developing a lesson. + elements of a lesson. These are often used when developing a lesson. - contents: - get_drafts - get_config @@ -50,7 +50,7 @@ reference: - strip_prefix - title: "The Pacakge Cache" desc: > - Lessons with generated content (R Markdown lessons) have an extra file + Lessons with generated content (R Markdown lessons) have an extra file called `renv/profiles/lesson-requirments/renv.lock` that records the package versions used to build the lesson. These functions provide ways for you to manage these packages and turn it on or off while previewing the @@ -58,6 +58,7 @@ reference: - contents: - use_package_cache - manage_deps + - use_python - title: "Updating Lesson Tools" desc: > Lesson updates will happen automatically on a regular schedule on GitHub. @@ -78,7 +79,7 @@ reference: - ci_session_info - git_worktree_setup - title: "[Internal] Build Components" - desc: > + desc: > Individual components to provision files and build a lesson locally - contents: - this_lesson @@ -131,4 +132,3 @@ articles: - title: "In Progress" contents: - articles/internationalisation - From 69c62b7d3eb91551161cd0b4354fcc83929db4c4 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 19 Jun 2023 11:42:58 +0200 Subject: [PATCH 24/47] Get R CMD check to succeed on `oldrel-4` --- tests/testthat/test-use_python.R | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R index a5940b912..88612ea9e 100644 --- a/tests/testthat/test-use_python.R +++ b/tests/testthat/test-use_python.R @@ -27,6 +27,7 @@ test_that("A warning is generated when reticulate is not installable", { test_that("use_python() sets reticulate configuration", { skip_on_os("windows") + skip_if_not(reticulate_installable, "reticulate is not installable") reticulate_python_env <- get_renv_env(lsn, "RETICULATE_PYTHON") py_config <- check_reticulate_config(lsn) From 9f99900f820a80b20d2ac5cc81a047333f256b92 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 23 Oct 2023 11:44:18 +0100 Subject: [PATCH 25/47] Make sure the correct project is loaded in renv --- R/use_python.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/use_python.R b/R/use_python.R index 889108639..3a48c1b1b 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -55,7 +55,7 @@ use_python <- function(path = ".", python = NULL, ## Set up working directory, avoids some renv side effects setwd(path) - renv::load(project = path) + renv::load(project = ".") # load the default renv profile from the working directory prof <- Sys.getenv("RENV_PROFILE") install_reticulate(path = path) From 5869aaf491dcd4cfaeeb3f9b8e86289c76a5bc27 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 30 Oct 2023 14:20:52 +0000 Subject: [PATCH 26/47] Tests: add `with_renv_profile()` helper Allows running relevant checks with the test lesson's renv profile. --- tests/testthat/helper-python.R | 47 +++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/tests/testthat/helper-python.R b/tests/testthat/helper-python.R index c75f50d59..ef5ad7df8 100644 --- a/tests/testthat/helper-python.R +++ b/tests/testthat/helper-python.R @@ -1,31 +1,36 @@ -## Helpers to temporarily load renv environment -local_load_py_pkg <- function(lsn, package) { - local_renv_load(lsn) - reticulate::import(package) -} - ## These helpers are used in `test-use_python.R`, they are implemented separately to ensure the ## temporary loading of the renv environment doesn't interfere with the testing environment + +## Allows running relevant checks with the test lesson's renv profile +with_renv_profile <- function(path, code, profile = "lesson-requirements", ...) { + path <- normalizePath(path) + code <- rlang::enexpr(code) + callr::r( + func = function(path, code) { + setwd(path) + renv::load(path) + eval(code) + }, + args = list(path = path, code = code), + env = c(callr::rcmd_safe_env(), "RENV_PROFILE" = profile), + ... + ) +} + +get_renv_env <- function(lsn, which = "RETICULATE_PYTHON") { + which <- rlang::enexpr(which) + with_renv_profile(lsn, Sys.getenv(!!which)) +} + check_reticulate <- function(lsn) { - local_renv_load(lsn) - lib <- renv::paths$library(project = lsn) - withr::local_libpaths(lib) - rlang::is_installed("reticulate") + with_renv_profile(lsn, rlang::is_installed("reticulate")) } check_reticulate_config <- function(lsn) { - local_renv_load(lsn) - reticulate::py_config() + with_renv_profile(lsn, reticulate::py_config()) } -get_renv_env <- function(lsn, which = "RETICULATE_PYTHON") { - local_renv_load(lsn) - Sys.getenv(which) +local_load_py_pkg <- function(lsn, package) { + with_renv_profile(lsn, reticulate::import(package)) } -## Temporarily load a renv profile, unloading it upon exit -local_renv_load <- function(lsn, env = parent.frame()) { - ## NOTE: renv:::unload() is currently not exported: https://github.com/rstudio/renv/issues/1285 - withr::defer(renv:::unload(project = lsn), envir = env) - renv::load(lsn) -} From 0ebc409c6aadb0692c4c47785fe7e460a192fdc1 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 30 Oct 2023 15:41:30 +0000 Subject: [PATCH 27/47] Add option to open lesson project in `use_python()` --- R/create_lesson.R | 2 +- R/use_python.R | 9 ++++++++- man/use_python.Rd | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/R/create_lesson.R b/R/create_lesson.R index 963e537da..b134bb501 100644 --- a/R/create_lesson.R +++ b/R/create_lesson.R @@ -106,7 +106,7 @@ create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio if (add_python) { cli::cli_status_update("{cli::symbol$arrow_right} Setting up Python ...") - use_python(path = path, python = python, type = type) + use_python(path = path, python = python, type = type, open = FALSE) } } diff --git a/R/use_python.R b/R/use_python.R index 3a48c1b1b..6e76dce13 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -5,6 +5,7 @@ #' #' @param path path to the current project #' @inheritParams renv::use_python +#' @param open if interactive, the lesson will open in a new editor window. #' @param ... Further arguments to be passed on to [renv::use_python()] #' #' @details @@ -43,7 +44,8 @@ #' py_install("numpy") #' } use_python <- function(path = ".", python = NULL, - type = c("auto", "virtualenv", "conda", "system"), ...) { + type = c("auto", "virtualenv", "conda", "system"), + open = rlang::is_interactive(), ...) { wd <- getwd() @@ -64,6 +66,11 @@ use_python <- function(path = ".", python = NULL, ## NOTE: use_python() deactivates the default profile, see https://github.com/rstudio/renv/issues/1217 ## Workaround: re-activate the profile renv::activate(project = path, profile = prof) + if (open) { + if (usethis::proj_activate(path)) { + on.exit() + } + } invisible(path) } diff --git a/man/use_python.Rd b/man/use_python.Rd index f6ce78715..898da81bd 100644 --- a/man/use_python.Rd +++ b/man/use_python.Rd @@ -9,6 +9,7 @@ use_python( path = ".", python = NULL, type = c("auto", "virtualenv", "conda", "system"), + open = rlang::is_interactive(), ... ) @@ -23,6 +24,8 @@ py_install(packages, path = ".", ...) \item{type}{The type of Python environment to use. When \code{"auto"} (the default), virtual environments will be used.} +\item{open}{if interactive, the lesson will open in a new editor window.} + \item{...}{Further arguments to be passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}} \item{packages}{Python packages to be installed as a character vecto.} From bd6249231c61dd866c94d0b804357f3133808e85 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 30 Oct 2023 15:44:54 +0000 Subject: [PATCH 28/47] Add `with_renv_factory()` helper --- R/utils-renv.R | 29 +++++++++++++++++++++++++++++ man/with_renv_factory.Rd | 28 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 man/with_renv_factory.Rd diff --git a/R/utils-renv.R b/R/utils-renv.R index b9470511e..c9839eb0e 100644 --- a/R/utils-renv.R +++ b/R/utils-renv.R @@ -313,3 +313,32 @@ callr_manage_deps <- function(path, repos, snapshot, lockfile_exists) { } return(NULL) } + +#' Generate a function to run in a renv profile +#' +#' This is a [Function operator](https://adv-r.hadley.nz/function-operators.html) which will +#' generate a function that will run in a renv profile. This is useful for running code in a +#' separate R subprocess with [`callr::r()`], to avoid *renv* side effects related to interactive +#' sessions. +#' +#' @param func The function to be evaluated after loading the renv environment. +#' @param renv_path The path to the renv environment to load. Usually a directory created by +#' [`create_lesson()`] +#' @param renv_profile Optional profile to load. Defaults to "lesson-requirements". +#' @param ... Additional arguments to be passed to `func`. +#' +#' @return The result of evaluating `func(...)` after loading the renv environment. +#' +#' @keywords internal +with_renv_factory <- function(func, renv_path, renv_profile = "lesson-requirements") { + force(func); force(renv_path); force(renv_profile) + + function(...) { + renv_path <- normalizePath(renv_path) + withr::local_dir(renv_path) + withr::local_envvar(c("RENV_PROFILE" = renv_profile)) + renv::load(renv_path) + + func(...) + } +} diff --git a/man/with_renv_factory.Rd b/man/with_renv_factory.Rd new file mode 100644 index 000000000..2faa358ec --- /dev/null +++ b/man/with_renv_factory.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-renv.R +\name{with_renv_factory} +\alias{with_renv_factory} +\title{Generate a function to run in a renv profile} +\usage{ +with_renv_factory(func, renv_path, renv_profile = "lesson-requirements") +} +\arguments{ +\item{func}{The function to be evaluated after loading the renv environment.} + +\item{renv_path}{The path to the renv environment to load. Usually a directory created by +\code{\link[=create_lesson]{create_lesson()}}} + +\item{renv_profile}{Optional profile to load. Defaults to "lesson-requirements".} + +\item{...}{Additional arguments to be passed to \code{func}.} +} +\value{ +The result of evaluating \code{func(...)} after loading the renv environment. +} +\description{ +This is a \href{https://adv-r.hadley.nz/function-operators.html}{Function operator} which will +generate a function that will run in a renv profile. This is useful for running code in a +separate R subprocess with \code{\link[callr:r]{callr::r()}}, to avoid \emph{renv} side effects related to interactive +sessions. +} +\keyword{internal} From 3a629cd6723c8bc4abb31e07f0b1881d8ab53487 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 30 Oct 2023 15:45:36 +0000 Subject: [PATCH 29/47] Refactor `use_python` functions to use `with_renv_factory()` Avoids renv side effects when running in interactive sessions --- R/use_python.R | 88 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/R/use_python.R b/R/use_python.R index 6e76dce13..17c8fd287 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -47,25 +47,30 @@ use_python <- function(path = ".", python = NULL, type = c("auto", "virtualenv", "conda", "system"), open = rlang::is_interactive(), ...) { - wd <- getwd() + ## Make sure reticulate is installed + install_reticulate(path = path) - ## Load the renv profile, unloading it upon exit - on.exit({ - invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) - setwd(wd) - }, add = TRUE, after = FALSE) + ## Generate function to run in separate R process + use_python_with_renv <- function(path, python, type, ...) { + prof <- Sys.getenv("RENV_PROFILE") + renv::use_python(project = path, python = python, type = type, ...) - ## Set up working directory, avoids some renv side effects - setwd(path) - renv::load(project = ".") # load the default renv profile from the working directory - prof <- Sys.getenv("RENV_PROFILE") + ## NOTE: use_python() deactivates the default profile, + ## see https://github.com/rstudio/renv/issues/1217 + ## Workaround: re-activate the profile + renv::activate(project = path, profile = prof) + } + callr_use_python <- with_renv_factory(use_python_with_renv, + renv_path = path, renv_profile = "lesson-requirements" + ) - install_reticulate(path = path) - renv::use_python(python = python, type = type, ...) + ## Run in separate R process + callr::r( + func = function(f, path, python, type, ...) f(path = path, python = python , type = type, ...), + args = list(f = callr_use_python, path = path, python = python, type = type, ...), + show = TRUE + ) - ## NOTE: use_python() deactivates the default profile, see https://github.com/rstudio/renv/issues/1217 - ## Workaround: re-activate the profile - renv::activate(project = path, profile = prof) if (open) { if (usethis::proj_activate(path)) { on.exit() @@ -90,32 +95,45 @@ use_python <- function(path = ".", python = NULL, #' @rdname use_python py_install <- function(packages, path = ".", ...) { - ## Load the renv profile, unloading it upon exit - renv::load(project = path) + ## Ensure reticulate is installed + install_reticulate(path = path) - on.exit({ - invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) - }, add = TRUE, after = FALSE) + py_install_with_renv <- function(packages, ...) { + reticulate::py_install(packages = packages, ...) + cli::cli_alert("Updating the package cache") + renv::snapshot(prompt = FALSE) + } + callr_py_install <- with_renv_factory(py_install_with_renv, + renv_path = path, renv_profile = "lesson-requirements" + ) - install_reticulate(path = path) - reticulate::py_install(packages = packages, ...) + ## Run in separate R process + callr::r( + func = function(f, packages) f(packages = packages), + args = list(f = callr_py_install, packages = packages), + show = TRUE + ) - cli::cli_alert("Updating the package cache") - renv::snapshot(lockfile = renv::paths$lockfile(project = path), prompt = FALSE) + invisible(TRUE) } -install_reticulate <- function(path) { - renv_lib <- renv::paths$library(project = path) - has_reticulate <- requireNamespace("reticulate", lib.loc = renv_lib, quietly = TRUE) - reticulate_installable <- check_reticulate_installable() - if (!has_reticulate && reticulate_installable) { - cli::cli_alert("Adding `reticulate` as a dependency") - ## Force reticulate to be recorded by renv - dep_file <- fs::path(path, "dependencies.R") - write("library(reticulate)", file = dep_file, append = TRUE) - renv::install("reticulate", library = renv_lib) + +## Helper to install reticulate in the lesson's renv environment and record it as a dependency +install_reticulate <- function(path, quiet = FALSE) { + + if (!check_reticulate_installable()) { + cli::cli_alert("`reticulate` can not be installed on this system. Skipping installation.") + return(invisible(FALSE)) } - invisible(NULL) + + ## Record reticulate as a dependency for renv + dep_file <- fs::path(path, "dependencies.R") + write("library(reticulate)", file = dep_file, append = TRUE) + + ## Install reticulate through manage_deps() + manage_deps(path = path, quiet = quiet) + + invisible(TRUE) } check_reticulate_installable <- function() { From 4833219130cd611115a09832f2955a7cf0ee9e29 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 30 Oct 2023 15:46:31 +0000 Subject: [PATCH 30/47] Bug fix for `local_load_py_pkg()`; need to unquote argument --- tests/testthat/helper-python.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/testthat/helper-python.R b/tests/testthat/helper-python.R index ef5ad7df8..c3d782f42 100644 --- a/tests/testthat/helper-python.R +++ b/tests/testthat/helper-python.R @@ -31,6 +31,7 @@ check_reticulate_config <- function(lsn) { } local_load_py_pkg <- function(lsn, package) { - with_renv_profile(lsn, reticulate::import(package)) + package <- rlang::enexpr(package) + with_renv_profile(lsn, reticulate::import(!!package)) } From fab1a3a88453f90cee8642b4354936e70c0cf28d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 30 Oct 2023 16:13:38 +0000 Subject: [PATCH 31/47] Load Python packages from renv environment in tests --- tests/testthat/test-manage_deps.R | 4 +--- tests/testthat/test-use_python.R | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 4d64e35b0..5ce75c138 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -240,14 +240,12 @@ test_that("manage_deps() restores Python dependencies", { skip_on_cran() skip_on_os("windows") - old_wd <- setwd(lsn) - withr::defer(setwd(old_wd)) use_python(lsn, type = "virtualenv") req_file <- fs::path(lsn, "requirements.txt") writeLines("numpy", req_file) res <- manage_deps(lsn, quiet = TRUE) - expect_no_error({numpy <- reticulate::import("numpy")}) + expect_no_error({numpy <- local_load_py_pkg(lsn, "numpy")}) expect_s3_class(numpy, "python.builtin.module") }) diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R index 88612ea9e..d97ac8a34 100644 --- a/tests/testthat/test-use_python.R +++ b/tests/testthat/test-use_python.R @@ -50,7 +50,7 @@ test_that("py_install() installs Python packages", { py_install("numpy", path = lsn) numpy <- local_load_py_pkg(lsn, "numpy") - expect_no_error({numpy <- reticulate::import("numpy")}) + expect_no_error({numpy <- local_load_py_pkg(lsn, "numpy")}) expect_s3_class(numpy, "python.builtin.module") req_file <- fs::path(lsn, "requirements.txt") From e7fe19668670f2c53541931853fbaab7382d693d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 7 Nov 2023 15:18:34 +0000 Subject: [PATCH 32/47] Create pull.yml --- .github/pull.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/pull.yml diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 000000000..ca7d27801 --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,8 @@ +version: "1" +rules: + - base: main + upstream: carpentries:main + mergeMethod: hardreset + mergeUnstable: false +label: ":arrow_heading_down: pull" # Optional +conflictLabel: "merge-conflict" # Optional, on merge conflict assign a custom label, Default: merge-conflict From 694f66d8c094794d6e73589b7b41cb1eb6b53d0d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 16 Nov 2023 12:18:32 +0100 Subject: [PATCH 33/47] Use default pull config --- .github/pull.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/pull.yml diff --git a/.github/pull.yml b/.github/pull.yml deleted file mode 100644 index ca7d27801..000000000 --- a/.github/pull.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: "1" -rules: - - base: main - upstream: carpentries:main - mergeMethod: hardreset - mergeUnstable: false -label: ":arrow_heading_down: pull" # Optional -conflictLabel: "merge-conflict" # Optional, on merge conflict assign a custom label, Default: merge-conflict From 6360ece023dfccca619a8b02531fbfee15df726a Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 4 Dec 2023 22:08:25 +0000 Subject: [PATCH 34/47] Fix `manage_deps()` Python-related tests Activate Python outside of `test_that()` blocks --- tests/testthat/test-manage_deps.R | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 5ce75c138..d3e5e60f7 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -215,6 +215,7 @@ test_that("update_cache() will update old package versions", { }) reticulate_installable <- check_reticulate_installable() +use_python(lsn, type = "virtualenv", open = FALSE) test_that("manage_deps() does not overwrite requirements.txt", { skip_if_not(reticulate_installable, "reticulate is not installable") @@ -226,7 +227,6 @@ test_that("manage_deps() does not overwrite requirements.txt", { ## Set up Python and manually add requirements.txt without actually installing ## the Python package, mimicking the scenario where a Python dependency is missing - use_python(lsn, type = "virtualenv") req_file <- fs::path(lsn, "requirements.txt") writeLines("numpy", req_file) @@ -240,8 +240,6 @@ test_that("manage_deps() restores Python dependencies", { skip_on_cran() skip_on_os("windows") - use_python(lsn, type = "virtualenv") - req_file <- fs::path(lsn, "requirements.txt") writeLines("numpy", req_file) res <- manage_deps(lsn, quiet = TRUE) From 32593ed969e02f879cd73819e2e4c81698fb9412 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 5 Dec 2023 15:59:51 +0000 Subject: [PATCH 35/47] Allow for multiple lines in `requirements.txt` during tests --- tests/testthat/test-manage_deps.R | 4 +++- tests/testthat/test-use_python.R | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index d3e5e60f7..da2769d40 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -228,10 +228,11 @@ test_that("manage_deps() does not overwrite requirements.txt", { ## Set up Python and manually add requirements.txt without actually installing ## the Python package, mimicking the scenario where a Python dependency is missing req_file <- fs::path(lsn, "requirements.txt") + if (file.exists(req_file)) fs::file_delete(req_file) writeLines("numpy", req_file) res <- manage_deps(lsn, quiet = TRUE) - expect_true(grepl("^numpy", readLines(req_file))) + expect_true(grepl("^numpy", readLines(req_file))[1]) }) @@ -241,6 +242,7 @@ test_that("manage_deps() restores Python dependencies", { skip_on_os("windows") req_file <- fs::path(lsn, "requirements.txt") + if (file.exists(req_file)) fs::file_delete(req_file) writeLines("numpy", req_file) res <- manage_deps(lsn, quiet = TRUE) diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R index d97ac8a34..5258b353e 100644 --- a/tests/testthat/test-use_python.R +++ b/tests/testthat/test-use_python.R @@ -54,5 +54,5 @@ test_that("py_install() installs Python packages", { expect_s3_class(numpy, "python.builtin.module") req_file <- fs::path(lsn, "requirements.txt") - expect_true(grepl("^numpy", readLines(req_file))) + expect_true(any(grepl("^numpy", readLines(req_file)))) }) From 57923a854620f6079322bde9fa5fc171c5ff5a9f Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 7 Mar 2024 18:24:16 +0000 Subject: [PATCH 36/47] Minor test fixes Correctly check for lines in `requirements.txt` --- tests/testthat/test-manage_deps.R | 5 +++-- tests/testthat/test-use_python.R | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index da2769d40..7cb82b03f 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -229,10 +229,11 @@ test_that("manage_deps() does not overwrite requirements.txt", { ## the Python package, mimicking the scenario where a Python dependency is missing req_file <- fs::path(lsn, "requirements.txt") if (file.exists(req_file)) fs::file_delete(req_file) - writeLines("numpy", req_file) + numpy_version <- "numpy==1.26.4" + writeLines(numpy_version, req_file) res <- manage_deps(lsn, quiet = TRUE) - expect_true(grepl("^numpy", readLines(req_file))[1]) + expect_true(numpy_version %in% readLines(req_file)) }) diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R index 5258b353e..5afbd5021 100644 --- a/tests/testthat/test-use_python.R +++ b/tests/testthat/test-use_python.R @@ -47,12 +47,15 @@ test_that("py_install() installs Python packages", { skip_if_not(reticulate_installable, "reticulate is not installable") skip_on_os("windows") - py_install("numpy", path = lsn) + numpy_version <- "numpy==1.26.4" + py_install(numpy_version, path = lsn) numpy <- local_load_py_pkg(lsn, "numpy") - expect_no_error({numpy <- local_load_py_pkg(lsn, "numpy")}) + expect_no_error({ + numpy <- local_load_py_pkg(lsn, "numpy") + }) expect_s3_class(numpy, "python.builtin.module") req_file <- fs::path(lsn, "requirements.txt") - expect_true(any(grepl("^numpy", readLines(req_file)))) + expect_true(numpy_version %in% readLines(req_file)) }) From 857421b01849355f0d8aa94f79c27d6ec948f725 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 3 Jun 2024 18:03:50 +0100 Subject: [PATCH 37/47] Add `quiet` option for `use_python` --- R/use_python.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/use_python.R b/R/use_python.R index 17c8fd287..ec7601fdc 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -45,10 +45,10 @@ #' } use_python <- function(path = ".", python = NULL, type = c("auto", "virtualenv", "conda", "system"), - open = rlang::is_interactive(), ...) { + open = rlang::is_interactive(), ..., quiet = FALSE) { ## Make sure reticulate is installed - install_reticulate(path = path) + install_reticulate(path = path, quiet = quiet) ## Generate function to run in separate R process use_python_with_renv <- function(path, python, type, ...) { @@ -68,7 +68,7 @@ use_python <- function(path = ".", python = NULL, callr::r( func = function(f, path, python, type, ...) f(path = path, python = python , type = type, ...), args = list(f = callr_use_python, path = path, python = python, type = type, ...), - show = TRUE + show = !quiet ) if (open) { From fe9311dba2e505a3d175ba21be1e50a02b504a33 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 3 Jun 2024 18:04:10 +0100 Subject: [PATCH 38/47] Test that `update_cache()` doesn't remove Python dependencies --- tests/testthat/test-manage_deps.R | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 7cb82b03f..07e9ec21f 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -215,7 +215,7 @@ test_that("update_cache() will update old package versions", { }) reticulate_installable <- check_reticulate_installable() -use_python(lsn, type = "virtualenv", open = FALSE) +use_python(lsn, type = "virtualenv", open = FALSE, quiet = TRUE) test_that("manage_deps() does not overwrite requirements.txt", { skip_if_not(reticulate_installable, "reticulate is not installable") @@ -250,3 +250,16 @@ test_that("manage_deps() restores Python dependencies", { expect_no_error({numpy <- local_load_py_pkg(lsn, "numpy")}) expect_s3_class(numpy, "python.builtin.module") }) + + +test_that("update_cache does not remove uninstalled Python dependencies from requirements.txt", { + skip_if_not(reticulate_installable, "reticulate is not installable") + skip_on_cran() + skip_on_os("windows") + + req_file <- fs::path(lsn, "requirements.txt") + writeLines("imnotinstalled", req_file) + + res <- update_cache(lsn, prompt = FALSE, quiet = TRUE) + expect_true("imnotinstalled" %in% readLines(req_file)) +}) From 5893b4d409e6d0f00a56c8226cf92a309f5dfbdc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 4 Jun 2024 12:45:21 +0100 Subject: [PATCH 39/47] Use a real Python package to test `update_cache()` --- tests/testthat/test-manage_deps.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 07e9ec21f..f80467f01 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -258,8 +258,8 @@ test_that("update_cache does not remove uninstalled Python dependencies from req skip_on_os("windows") req_file <- fs::path(lsn, "requirements.txt") - writeLines("imnotinstalled", req_file) + writeLines("pandas", req_file) res <- update_cache(lsn, prompt = FALSE, quiet = TRUE) - expect_true("imnotinstalled" %in% readLines(req_file)) + expect_true("pandas" %in% readLines(req_file)) }) From 61192277548c81eaa7112962d77dcd5670724877 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 4 Jun 2024 12:46:00 +0100 Subject: [PATCH 40/47] Make sure current requirements are installed before updating cache Fixes https://github.com/LearnToDiscover/sandpaper/issues/68 --- R/manage_deps.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/R/manage_deps.R b/R/manage_deps.R index 8a65efcbf..91b6c90e3 100644 --- a/R/manage_deps.R +++ b/R/manage_deps.R @@ -112,6 +112,11 @@ update_cache <- function(path = ".", profile = "lesson-requirements", prompt = i Sys.setenv("RENV_PROFILE" = prof) }, add = TRUE) Sys.setenv("RENV_PROFILE" = profile) + + # Make sure the current requirements are installed, this also avoids removing any uninstalled + # Python dependencies in requirements.txt + manage_deps(path = path, profile = profile, snapshot = snapshot, quiet = quiet) + renv::load(project = path) lib <- renv::paths$library(project = path) if (prompt) { From 2131f99f88ba03d090142a65cffaab270153036d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 6 Jun 2024 15:38:13 +0100 Subject: [PATCH 41/47] Don't overwrite existing `requirements.txt` file in tests --- tests/testthat/test-manage_deps.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index f80467f01..7303e058b 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -258,8 +258,9 @@ test_that("update_cache does not remove uninstalled Python dependencies from req skip_on_os("windows") req_file <- fs::path(lsn, "requirements.txt") - writeLines("pandas", req_file) + write("pandas", req_file, append = TRUE, sep = "\n") res <- update_cache(lsn, prompt = FALSE, quiet = TRUE) - expect_true("pandas" %in% readLines(req_file)) + print(readLines(req_file)) + expect_true(any(grepl("pandas", readLines(req_file)))) }) From d28e74a72dca8279fcb487b71f983e2acc4f4cde Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 7 Jun 2024 16:04:22 +0100 Subject: [PATCH 42/47] Make sure `with_renv_factory` deactivates renv upon completion --- R/utils-renv.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/R/utils-renv.R b/R/utils-renv.R index c9839eb0e..81cbbd880 100644 --- a/R/utils-renv.R +++ b/R/utils-renv.R @@ -314,6 +314,7 @@ callr_manage_deps <- function(path, repos, snapshot, lockfile_exists) { return(NULL) } + #' Generate a function to run in a renv profile #' #' This is a [Function operator](https://adv-r.hadley.nz/function-operators.html) which will @@ -334,6 +335,9 @@ with_renv_factory <- function(func, renv_path, renv_profile = "lesson-requiremen force(func); force(renv_path); force(renv_profile) function(...) { + on.exit({ + invisible(utils::capture.output(renv::deactivate(project = renv_path), type = "message")) + }, add = TRUE) renv_path <- normalizePath(renv_path) withr::local_dir(renv_path) withr::local_envvar(c("RENV_PROFILE" = renv_profile)) @@ -342,3 +346,5 @@ with_renv_factory <- function(func, renv_path, renv_profile = "lesson-requiremen func(...) } } + + From 94c5c9cd36f5af5b1d1d35c228a0d2a5c87d1d10 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 7 Jun 2024 16:06:00 +0100 Subject: [PATCH 43/47] Refactor `update_cache()` to use `with_renv_factory()` --- R/manage_deps.R | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/R/manage_deps.R b/R/manage_deps.R index 91b6c90e3..e046f6ace 100644 --- a/R/manage_deps.R +++ b/R/manage_deps.R @@ -104,23 +104,31 @@ manage_deps <- function(path = ".", profile = "lesson-requirements", snapshot = #' if it's running in an interactive session. #' @rdname dependency_management #' @export -update_cache <- function(path = ".", profile = "lesson-requirements", prompt = interactive(), quiet = !prompt, snapshot = TRUE) { - path <- root_path(path) +update_cache <- function(path = ".", profile = "lesson-requirements", prompt = interactive(), + quiet = !prompt, snapshot = TRUE) { + prof <- Sys.getenv("RENV_PROFILE") on.exit({ - invisible(utils::capture.output(renv::deactivate(path), type = "message")) Sys.setenv("RENV_PROFILE" = prof) }, add = TRUE) Sys.setenv("RENV_PROFILE" = profile) + path <- root_path(path) # Make sure the current requirements are installed, this also avoids removing any uninstalled # Python dependencies in requirements.txt manage_deps(path = path, profile = profile, snapshot = snapshot, quiet = quiet) - renv::load(project = path) lib <- renv::paths$library(project = path) + updates <- callr::r( + func = function(f, lib) f(lib), + args = list( + f = with_renv_factory(check_for_updates, renv_path = path, renv_profile = profile), + lib = lib + ), + show = !quiet + ) + if (prompt) { - updates <- renv::update(library = lib, check = TRUE, prompt = TRUE) if (isTRUE(updates)) { return(invisible()) } @@ -140,6 +148,33 @@ update_cache <- function(path = ".", profile = "lesson-requirements", prompt = i return(invisible()) } } + + sho <- !(quiet || identical(Sys.getenv("TESTTHAT"), "true")) + out <- callr::r( + func = function(f, path, lib, snapshot) f(path, lib, snapshot), + args = list( + f = with_renv_factory(callr_update_cache, renv_path = path, renv_profile = profile), + path = path, + lib = lib, + snapshot = snapshot + ), + show = !quiet, + spinner = sho, + user_profile = FALSE, + env = c(callr::rcmd_safe_env(), + "R_PROFILE_USER" = fs::path(tempfile(), "nada"), + "RENV_CONFIG_CACHE_SYMLINKS" = renv_cache_available() + ) + ) + invisible(out) +} + +check_for_updates <- function(lib) { + cli::cli_alert("Checking for updates") + renv::update(library = lib, check = TRUE, prompt = TRUE) +} + +callr_update_cache <- function(path, lib, snapshot) { updates <- renv::update(library = lib, prompt = FALSE) if (snapshot) { renv::snapshot(lockfile = renv::paths$lockfile(project = path), prompt = FALSE) From 46318692d6706ea6f92576d6fb8ae5c5cd5d37ef Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 7 Jun 2024 16:06:39 +0100 Subject: [PATCH 44/47] Clean up tests --- tests/testthat/test-manage_deps.R | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 7303e058b..08b565592 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -206,10 +206,13 @@ test_that("update_cache() will update old package versions", { skip_if_offline() skip_if(covr::in_covr()) + pkg <- "sessioninfo" + old_pkg_version <- "1.1.0" + pin_version(glue::glue("{pkg}@{old_pkg_version}"), path = fs::path(lsn, "episodes")) res <- update_cache(path = fs::path(lsn, "episodes"), prompt = FALSE, quiet = FALSE) expect_true( - package_version(res$sessioninfo$Version) > package_version("1.1.0") + package_version(res[[pkg]]$Version) > package_version(old_pkg_version) ) }) @@ -260,7 +263,6 @@ test_that("update_cache does not remove uninstalled Python dependencies from req req_file <- fs::path(lsn, "requirements.txt") write("pandas", req_file, append = TRUE, sep = "\n") - res <- update_cache(lsn, prompt = FALSE, quiet = TRUE) - print(readLines(req_file)) + res <- update_cache(lsn, prompt = FALSE, quiet = FALSE) expect_true(any(grepl("pandas", readLines(req_file)))) }) From 3540c9929036ba3eb6ed81c5b66d490925afce51 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 7 Jun 2024 16:21:22 +0100 Subject: [PATCH 45/47] roxygenise --- DESCRIPTION | 2 +- NAMESPACE | 1 + man/known_languages.Rd | 1 + man/template.Rd | 3 +++ man/use_python.Rd | 3 ++- man/yaml_list.Rd | 4 +++- 6 files changed, 11 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index ff9c775bb..cbb361b71 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -108,7 +108,7 @@ Config/testthat/parallel: false Config/Needs/check: rstudio/renv Config/potools/style: explicit Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 URL: https://carpentries.github.io/sandpaper/, https://github.com/carpentries/sandpaper/, https://carpentries.github.io/workbench/ BugReports: https://github.com/carpentries/sandpaper/issues/ VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 6d9062aa8..e0ade642d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -38,6 +38,7 @@ export(set_instructors) export(set_learners) export(set_profiles) export(strip_prefix) +export(template_citation) export(template_conduct) export(template_config) export(template_contributing) diff --git a/man/known_languages.Rd b/man/known_languages.Rd index 4ee2b95b7..0fcef5554 100644 --- a/man/known_languages.Rd +++ b/man/known_languages.Rd @@ -22,6 +22,7 @@ details of how to do so in the source code for {sandpaper}. \if{html}{\out{
}}\preformatted{#> - en #> - es +#> - fr #> - ja #> - uk }\if{html}{\out{
}} diff --git a/man/template.Rd b/man/template.Rd index 72f4105d7..1f1f23a2b 100644 --- a/man/template.Rd +++ b/man/template.Rd @@ -4,6 +4,7 @@ \alias{template_gitignore} \alias{template_episode} \alias{template_links} +\alias{template_citation} \alias{template_config} \alias{template_conduct} \alias{template_index} @@ -23,6 +24,8 @@ template_episode() template_links() +template_citation() + template_config() template_conduct() diff --git a/man/use_python.Rd b/man/use_python.Rd index 898da81bd..71fd912e0 100644 --- a/man/use_python.Rd +++ b/man/use_python.Rd @@ -10,7 +10,8 @@ use_python( python = NULL, type = c("auto", "virtualenv", "conda", "system"), open = rlang::is_interactive(), - ... + ..., + quiet = FALSE ) py_install(packages, path = ".", ...) diff --git a/man/yaml_list.Rd b/man/yaml_list.Rd index aea4ad033..c6edd157c 100644 --- a/man/yaml_list.Rd +++ b/man/yaml_list.Rd @@ -25,7 +25,9 @@ cat(yaml::as.yaml(hx)) # representation in yaml #> - a #> - b #> - c -cat(whisker::whisker.render("hello: \{\{hello\}\}", hx)) # messed up whisker +}\if{html}{\out{}} + +\if{html}{\out{
}}\preformatted{cat(whisker::whisker.render("hello: \{\{hello\}\}", hx)) # messed up whisker #> hello: a,b,c }\if{html}{\out{
}} From 2330acd9f472f15a62fb26235cd403f5964da077 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 7 Jun 2024 16:33:28 +0100 Subject: [PATCH 46/47] Add mising param doc --- R/use_python.R | 1 + man/use_python.Rd | 2 ++ 2 files changed, 3 insertions(+) diff --git a/R/use_python.R b/R/use_python.R index ec7601fdc..6f6db95bf 100644 --- a/R/use_python.R +++ b/R/use_python.R @@ -6,6 +6,7 @@ #' @param path path to the current project #' @inheritParams renv::use_python #' @param open if interactive, the lesson will open in a new editor window. +#' @param quiet if `TRUE`, suppresses output. #' @param ... Further arguments to be passed on to [renv::use_python()] #' #' @details diff --git a/man/use_python.Rd b/man/use_python.Rd index 71fd912e0..177995e78 100644 --- a/man/use_python.Rd +++ b/man/use_python.Rd @@ -29,6 +29,8 @@ virtual environments will be used.} \item{...}{Further arguments to be passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}} +\item{quiet}{if \code{TRUE}, suppresses output.} + \item{packages}{Python packages to be installed as a character vecto.} } \value{ From 71432420dac388037d3353d50ffb5c5bb5eff73d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 7 Jun 2024 17:43:53 +0100 Subject: [PATCH 47/47] Use smaller Python package to speed up test --- tests/testthat/test-manage_deps.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 08b565592..576f6b7b7 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -261,8 +261,8 @@ test_that("update_cache does not remove uninstalled Python dependencies from req skip_on_os("windows") req_file <- fs::path(lsn, "requirements.txt") - write("pandas", req_file, append = TRUE, sep = "\n") + write("art", req_file, append = TRUE, sep = "\n") res <- update_cache(lsn, prompt = FALSE, quiet = FALSE) - expect_true(any(grepl("pandas", readLines(req_file)))) + expect_true(any(grepl("art", readLines(req_file)))) })