Skip to content

Commit

Permalink
Merge pull request #15 from milanmlft/use_python
Browse files Browse the repository at this point in the history
Add support for setting up Python through renv
  • Loading branch information
sabaferdous12 authored May 26, 2023
2 parents 5bd7ace + d096467 commit eddf254
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 6 deletions.
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -64,7 +64,8 @@ Suggests:
jsonlite,
sessioninfo,
mockr,
varnish (>= 0.2.1)
varnish (>= 0.2.1),
reticulate
Additional_repositories: https://carpentries.r-universe.dev/
Remotes:
ropensci/tinkr,
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -54,6 +55,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)
Expand Down
18 changes: 17 additions & 1 deletion R/create_lesson.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +29,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}}...")
Expand Down Expand Up @@ -92,6 +103,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 ...")
Expand Down
111 changes: 111 additions & 0 deletions R/use_python.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#' Add Python as a lesson dependency
#'
#' 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()]
#'
#' @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
#' @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.
#' @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"), ...) {

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, 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)
}


#' Install Python packages and add them to the cache
#'
#' 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()]
#'
#' @export
#' @rdname use_python
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, after = FALSE)

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 <- 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
dep_file <- fs::path(path, "dependencies.R")
write("library(reticulate)", file = dep_file, append = TRUE)
renv::install("reticulate", library = renv_lib)
}
invisible(NULL)
}
8 changes: 6 additions & 2 deletions R/utils-renv.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion man/create_lesson.Rd

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

76 changes: 76 additions & 0 deletions man/use_python.Rd

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

31 changes: 31 additions & 0 deletions tests/testthat/helper-python.R
Original file line number Diff line number Diff line change
@@ -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)
}
34 changes: 34 additions & 0 deletions tests/testthat/test-manage_deps.R
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,37 @@ 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))

## 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", {
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_s3_class(numpy, "python.builtin.module")
})
Loading

0 comments on commit eddf254

Please sign in to comment.