Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for setting up Python through renv #448

Open
wants to merge 81 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
70027f9
Set dev version
milanmlft Apr 28, 2023
51c6995
Add `use_python()` helper
milanmlft May 2, 2023
9623395
Add `py_install()` helpeer to add Python dependencies
milanmlft May 2, 2023
1495f89
Reactivate renv profile after adding Python
milanmlft May 3, 2023
cea48bf
Make sure Python dependencies also get restored by `manage_deps()`
milanmlft May 3, 2023
28425e8
Add option to set up Python in `create_lesson()`
milanmlft May 3, 2023
b729c0c
Set working directory while setting up Python
milanmlft May 3, 2023
5ffacc6
Update documentation for `create_lesson()`
milanmlft May 3, 2023
669141c
Update documentation for `use_python()`
milanmlft May 3, 2023
0b45377
Reoxygenize
milanmlft May 3, 2023
4529311
Add examples for Python usage
milanmlft May 3, 2023
ec2599f
Add unit tests for Python integration
milanmlft May 3, 2023
cc05834
Also test that `manage_deps()` plays nice with Python integration
milanmlft May 3, 2023
1dd787a
Document
zkamvar May 3, 2023
41edc10
Merge branch 'main' into use_python
milanmlft May 17, 2023
df07005
Refactor: move reticulate installation to separate helper
milanmlft May 22, 2023
7deb07d
Improve tests, add helpers to manage renv during testing
milanmlft May 22, 2023
ac15e6c
Use `requireNamespace()` instead of `require()`
milanmlft May 22, 2023
76c814d
Merge branch 'main' into use_python
milanmlft May 22, 2023
2df6d49
Set dev version
milanmlft May 22, 2023
d096467
Skip `manage_deps()` tests on Windows
milanmlft May 22, 2023
fde134b
Skip `use_python` tests on Windows
milanmlft Jun 7, 2023
680c74e
Check that reticulate can be installed and warn if it does not
milanmlft Jun 8, 2023
a3a4d91
Merge branch 'main' into use_python
milanmlft Jun 9, 2023
da24671
Small bug fix
milanmlft Jun 8, 2023
a589e1c
Add `use_python` to pkgdown config
milanmlft Jun 14, 2023
69c62b7
Get R CMD check to succeed on `oldrel-4`
milanmlft Jun 19, 2023
9ed3a0b
Merge branch 'main' into use_python
zkamvar Sep 8, 2023
e127abe
Merge branch 'main' into use_python
milanmlft Oct 16, 2023
b467cea
Merge branch 'main' into use_python
milanmlft Oct 23, 2023
9f99900
Make sure the correct project is loaded in renv
milanmlft Oct 23, 2023
5869aaf
Tests: add `with_renv_profile()` helper
milanmlft Oct 30, 2023
0ebc409
Add option to open lesson project in `use_python()`
milanmlft Oct 30, 2023
bd62492
Add `with_renv_factory()` helper
milanmlft Oct 30, 2023
3a629cd
Refactor `use_python` functions to use `with_renv_factory()`
milanmlft Oct 30, 2023
4833219
Bug fix for `local_load_py_pkg()`; need to unquote argument
milanmlft Oct 30, 2023
fab1a3a
Load Python packages from renv environment in tests
milanmlft Oct 30, 2023
e7fe196
Create pull.yml
milanmlft Nov 7, 2023
90ab41c
Merge pull request #1 from carpentries/main
milanmlft Nov 7, 2023
7947908
Merge pull request #2 from carpentries/main
milanmlft Nov 10, 2023
65f7234
Merge pull request #3 from carpentries/main
milanmlft Nov 13, 2023
5a63272
Merge pull request #4 from carpentries/main
milanmlft Nov 15, 2023
d34036d
Merge pull request #5 from carpentries/main
milanmlft Nov 16, 2023
694f66d
Use default pull config
milanmlft Nov 16, 2023
99a46b4
Merge pull request #7 from carpentries/main
milanmlft Nov 22, 2023
753a111
Merge pull request #8 from carpentries/main
milanmlft Nov 23, 2023
31ba581
Merge pull request #9 from carpentries/main
milanmlft Nov 30, 2023
87d29d7
Merge remote-tracking branch 'upstream/main' into use_python
milanmlft Dec 4, 2023
6360ece
Fix `manage_deps()` Python-related tests
milanmlft Dec 4, 2023
32593ed
Allow for multiple lines in `requirements.txt` during tests
milanmlft Dec 5, 2023
47d803a
Merge pull request #10 from carpentries/main
milanmlft Dec 12, 2023
6d69475
Merge pull request #11 from carpentries/main
milanmlft Dec 13, 2023
63067e2
Merge pull request #12 from carpentries/main
milanmlft Dec 15, 2023
85259cf
Merge pull request #13 from carpentries/main
milanmlft Dec 21, 2023
da74bb1
Merge pull request #14 from carpentries/main
milanmlft Jan 15, 2024
8a9b751
Merge pull request #15 from carpentries/main
milanmlft Jan 23, 2024
4204aa3
Merge pull request #16 from carpentries/main
milanmlft Mar 6, 2024
5f3580f
Merge branch 'main' into use_python
milanmlft Mar 7, 2024
57923a8
Minor test fixes
milanmlft Mar 7, 2024
e5595ba
Merge pull request #17 from carpentries/main
milanmlft Mar 18, 2024
7ad44c8
Merge pull request #18 from carpentries/main
milanmlft Apr 8, 2024
430a37b
Merge pull request #19 from carpentries/main
milanmlft Apr 17, 2024
c98d9f0
Merge pull request #20 from carpentries/main
milanmlft Apr 22, 2024
d9fc5a7
Merge pull request #21 from carpentries/main
milanmlft May 10, 2024
bf8a3a8
Merge branch 'main' into use_python
milanmlft May 10, 2024
0b96097
Merge pull request #22 from carpentries/main
milanmlft May 30, 2024
8b51785
Merge branch 'main' of github.com:carpentries/sandpaper
milanmlft Jun 3, 2024
6243835
Merge branch 'main' of github.com:milanmlft/sandpaper
milanmlft Jun 3, 2024
1fa6880
Merge branch 'main' into use_python
milanmlft Jun 3, 2024
857421b
Add `quiet` option for `use_python`
milanmlft Jun 3, 2024
fe9311d
Test that `update_cache()` doesn't remove Python dependencies
milanmlft Jun 3, 2024
5893b4d
Use a real Python package to test `update_cache()`
milanmlft Jun 4, 2024
6119227
Make sure current requirements are installed before updating cache
milanmlft Jun 4, 2024
2131f99
Don't overwrite existing `requirements.txt` file in tests
milanmlft Jun 6, 2024
d28e74a
Make sure `with_renv_factory` deactivates renv upon completion
milanmlft Jun 7, 2024
94c5c9c
Refactor `update_cache()` to use `with_renv_factory()`
milanmlft Jun 7, 2024
4631869
Clean up tests
milanmlft Jun 7, 2024
3540c99
roxygenise
milanmlft Jun 7, 2024
2330acd
Add mising param doc
milanmlft Jun 7, 2024
7143242
Use smaller Python package to speed up test
milanmlft Jun 7, 2024
e070c7d
Merge branch 'main' into use_python
milanmlft Nov 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ Suggests:
jsonlite,
sessioninfo,
mockr,
varnish (>= 0.3.0)
varnish (>= 0.3.0),
reticulate
Additional_repositories: https://carpentries.r-universe.dev/
Remotes:
carpentries/pegboard,
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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 @@ -56,6 +57,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 @@ -93,6 +104,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, open = FALSE)
}
}

cli::cli_status_update("{cli::symbol$arrow_right} Committing ...")
Expand Down
50 changes: 45 additions & 5 deletions R/manage_deps.R
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +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)
renv::load(project = path)
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)

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())
}
Expand All @@ -135,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)
Expand Down
151 changes: 151 additions & 0 deletions R/use_python.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#' 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 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
#' 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"),
open = rlang::is_interactive(), ..., quiet = FALSE) {

## Make sure reticulate is installed
install_reticulate(path = path, quiet = quiet)

## 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, ...)

## 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"
)

## 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 = !quiet
)

if (open) {
if (usethis::proj_activate(path)) {
on.exit()
}
}
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 = ".", ...) {

## Ensure reticulate is installed
install_reticulate(path = path)

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"
)

## Run in separate R process
callr::r(
func = function(f, packages) f(packages = packages),
args = list(f = callr_py_install, packages = packages),
show = TRUE
)

invisible(TRUE)
}


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

## 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() {
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")
}
r_compatible
}

is_r_version_greater_than <- function(minimal_major = 4) {
R.version$major >= minimal_major
}
43 changes: 41 additions & 2 deletions R/utils-renv.R
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,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 All @@ -309,3 +313,38 @@ 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(...) {
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))
renv::load(renv_path)

func(...)
}
}


2 changes: 1 addition & 1 deletion _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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.
Expand Down Expand Up @@ -151,4 +152,3 @@ articles:
- title: "In Progress"
contents:
- articles/internationalisation

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.

Loading
Loading