R/virtualenv.R

Defines functions virtualenv_list virtualenv_create virtualenv_install virtualenv_remove virtualenv_root virtualenv_python virtualenv_exists virtualenv_path virtualenv_pip virtualenv_default_python virtualenv_module is_virtualenv

Documented in virtualenv_create virtualenv_install virtualenv_list virtualenv_python virtualenv_remove virtualenv_root

#' Interface to Python Virtual Environments
#'
#' R functions for managing Python [virtual environments](https://virtualenv.pypa.io/en/stable/).
#'
#' Virtual environments are by default located at `~/.virtualenvs` (accessed
#' with the `virtualenv_root` function). You can change the default location by
#' defining defining the `WORKON_HOME` environment variable.
#'
#' Virtual environment functions are not supported on Windows (the use of
#' [conda environments][conda-tools] is recommended on Windows).
#'
#' @param envname The name of, or path to, a Python virtual environment. If
#'   this name contains any slashes, the name will be interpreted as a path;
#'   if the name does not contain slashes, it will be treated as a virtual
#'   environment within `virtualenv_root()`. When `NULL`, the virtual environment
#'   as specified by the `RETICULATE_PYTHON_ENV` environment variable will be
#'   used instead.
#'
#' @param packages A character vector with package names to install or remove.
#'
#' @param ignore_installed Boolean; ignore previously-installed versions of the
#'   requested packages? (This should normally be `TRUE`, so that pre-installed
#'   packages available in the site libraries are ignored and hence packages
#'   are installed into the virtual environment.)
#'
#' @param confirm Boolean; confirm before removing packages or virtual
#'   environments?
#'
#' @param python The path to a Python interpreter, to be used with the created
#'   virtual environment. When `NULL`, the Python interpreter associated with
#'   the current session will be used.
#'
#' @param ... Optional arguments; currently ignored for future expansion.
#'
#' @name virtualenv-tools
NULL



#' @inheritParams virtualenv-tools
#' @rdname virtualenv-tools
#' @export
virtualenv_list <- function() {
  root <- virtualenv_root()
  if (!file.exists(root))
    return(character())
  list.files(root)
}

#' @inheritParams virtualenv-tools
#' @rdname virtualenv-tools
#' @export
virtualenv_create <- function(envname = NULL, python = NULL) {

  path <- virtualenv_path(envname)
  name <- if (is.null(envname)) path else envname

  # check and see if we already have a virtual environment
  if (virtualenv_exists(path)) {
    writeLines(paste("virtualenv:", name))
    return(invisible(path))
  }

  python <- virtualenv_default_python(python)
  module <- virtualenv_module(python)

  writeLines(paste("Creating virtual environment", shQuote(name), "..."))
  writeLines(paste("Using python:", python))

  # use it to create the virtual environment (note that 'virtualenv'
  # requires us to request the specific Python binary we wish to use when
  # creating the environment)
  args <- c("-m", module)
  if (module == "virtualenv")
    args <- c(args, "-p", shQuote(python))
  args <- c(args, "--system-site-packages", shQuote(path.expand(path)))
  
  result <- system2(python, args)
  if (result != 0L) {
    fmt <- "Error creating virtual environment '%s' [error code %d]"
    msg <- sprintf(fmt, name, result)
    stop(msg, call. = FALSE)
  }

  # upgrade pip and friends after creating the environment
  # (since the version bundled with virtualenv / venv may be stale)
  pip_install(python, c("pip", "wheel", "setuptools"))

  invisible(path)
}



#' @inheritParams virtualenv-tools
#' @rdname virtualenv-tools
#' @export
virtualenv_install <- function(envname = NULL,
                               packages,
                               ignore_installed = TRUE,
                               ...)
{
  # create virtual environment on demand
  path <- virtualenv_path(envname)
  if (!file.exists(path))
    path <- virtualenv_create(envname)

  # validate that we've received the path to a virtual environment
  name <- if (is.null(envname)) path else envname
  if (!is_virtualenv(path)) {
    fmt <- "'%s' exists but is not a virtual environment"
    stop(sprintf(fmt, name))
  }

  writeLines(paste("Using virtual environment", shQuote(name), "..."))

  # ensure that pip + friends are up-to-date / recent enough
  python <- virtualenv_python(envname)
  if (pip_version(python) > "8.1") {
    pip_install(python, "pip")
    pip_install(python, "wheel")
    pip_install(python, "setuptools")
  }

  # now install the requested package
  pip_install(python, packages, ignore_installed = ignore_installed)
}



#' @inheritParams virtualenv-tools
#' @rdname virtualenv-tools
#' @export
virtualenv_remove <- function(envname = NULL, packages = NULL, confirm = interactive()) {

  path <- virtualenv_path(envname)
  name <- if (is.null(envname)) path else envname

  if (!virtualenv_exists(path)) {
    fmt <- "Virtual environment '%s' does not exist."
    stop(sprintf(fmt, name), call. = FALSE)
  }

  # packages = NULL means remove the entire virtualenv
  if (is.null(packages)) {

    if (confirm) {
      fmt <- "Remove virtual environment '%s'? [Y/n]: "
      prompt <- sprintf(fmt, name)
      response <- readline(prompt = prompt)
      if (tolower(response) != "y") {
        writeLines("Operation aborted.")
        return(invisible(NULL))
      }
    }

    unlink(path, recursive = TRUE)
    writeLines(paste("Virtual environment", shQuote(name), "removed."))
    return(invisible(NULL))

  }

  # otherwise, remove the requested packages
  if (confirm) {
    fmt <- "Remove '%s' from virtual environment '%s'? [Y/n]: "
    prompt <- sprintf(fmt, paste(packages, sep = ", "), path)
    response <- readline(prompt = prompt)
    if (tolower(response) != "y") {
      writeLines("Operation aborted.")
      return(invisible(NULL))
    }
  }

  python <- virtualenv_python(envname)
  pip_uninstall(python, packages)
  invisible(NULL)
}



#' @inheritParams virtualenv-tools
#' @rdname virtualenv-tools
#' @export
virtualenv_root <- function() {
  Sys.getenv("WORKON_HOME", unset = "~/.virtualenvs")
}



#' @inheritParams virtualenv-tools
#' @rdname virtualenv-tools
#' @export
virtualenv_python <- function(envname = NULL) {
  path <- virtualenv_path(envname)
  path.expand(file.path(path, "bin/python"))
}



virtualenv_exists <- function(envname = NULL) {

  # try to resolve path
  path <- tryCatch(virtualenv_path(envname), error = identity)
  if (inherits(path, "error"))
    return(FALSE)

  # check for expected files for virtualenv / venv
  files <- c("bin/activate_this.py", "bin/pyvenv.cfg", "pyvenv.cfg")
  any(file.exists(file.path(path, files)))

}



virtualenv_path <- function(envname = NULL) {

  python_environment_resolve(
    envname = envname,
    resolve = function(envname) file.path(virtualenv_root(), envname)
  )

}


virtualenv_pip <- function(envname) {
  path <- virtualenv_path(envname)
  file.path(path, "bin/pip")
}



virtualenv_default_python <- function(python = NULL) {

  # if the user has supplied a verison of python already, use it
  if (!is.null(python))
    return(python)

  # check for some pre-defined Python sources (prefer Python 3)
  pythons <- c(
    Sys.getenv("RETICULATE_PYTHON"),
    .globals$required_python_version,
    Sys.which("python3"),
    Sys.which("python")
  )

  for (python in pythons) {

    # skip non-existent Python
    if (!file.exists(python))
      next

    # get list of required modules
    version <- tryCatch(python_version(python), error = identity)
    if (inherits(version, "error"))
      next

    py2_modules <- c("pip", "virtualenv")
    py3_modules <- c("pip", "venv")
    modules <- ifelse(version < 3, py2_modules, py3_modules)

    # ensure these modules are available
    if (!python_has_modules(python, modules))
      next

    return(normalizePath(python, winslash = "/"))

  }

  # otherwise, try to explicitly detect Python
  config <- py_discover_config()
  normalizePath(config$python, winslash = "/")

}



virtualenv_module <- function(python) {
  py_version <- python_version(python)

  # prefer 'venv' for Python 3, but allow for 'virtualenv' for both
  # (note that 'venv' and 'virtualenv' are largely compatible)
  modules <- "virtualenv"
  if (py_version >= "3.6")
    modules <- c("venv", modules)

  # if we have one of these modules available, return it
  for (module in modules)
    if (python_has_module(python, module))
      return(module)

  # virtualenv not available: instruct the user to install
  commands <- new_stack()
  commands$push("Tools for managing Python virtual environments are not installed.")
  commands$push("")

  # if we don't have pip, recommend its installation
  if (!python_has_module(python, "pip")) {
    commands$push("Install pip with:")
    if (python_has_module(python, "easy_install")) {
      commands$push(paste("$", python, "-m easy_install --upgrade --user pip"))
    } else if (is_ubuntu() && dirname(python) == "/usr/bin") {
      package <- if (py_version < 3) "python-pip" else "python3-pip"
      commands$push(paste("$ sudo apt-get install", package))
    } else {
      commands$push("$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py")
      commands$push(paste("$", python, "get-pip.py"))
    }
    commands$push("")
  }

  # then, recommend installation of virtualenv or venv with pip
  commands$push(paste("Install", modules[[1]], "with:"))
  if (is_ubuntu() && dirname(python) == "/usr/bin") {
    package <- if (py_version < 3) "python-virtualenv" else "python3-venv"
    commands$push(paste("$ sudo apt-get install", package))
  } else {
    commands$push(paste("$", python, "-m pip install --upgrade --user", module))
  }
  commands$push("\n")

  # report to user
  message <- paste(commands$data(), collapse = "\n")
  stop(message, call. = FALSE)

}



is_virtualenv <- function(dir) {
  files <- c("bin/activate_this.py", "bin/pyvenv.cfg", "pyvenv.cfg")
  any(file.exists(file.path(dir, files)))
}
merlingrace/reticulate1 documentation built on Nov. 4, 2019, 6:25 p.m.