File: python-packages.R

package info (click to toggle)
r-cran-reticulate 1.41.0.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,088 kB
  • sloc: cpp: 5,154; python: 620; sh: 13; makefile: 2
file content (257 lines) | stat: -rw-r--r-- 7,206 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257

#' Configure a Python Environment
#'
#' Configure a Python environment, satisfying the Python dependencies of any
#' loaded \R packages.
#'
#' Normally, this function should only be used by package authors, who want
#' to ensure that their package dependencies are installed in the active
#' Python environment. For example:
#'
#' ```
#' .onLoad <- function(libname, pkgname) {
#'   reticulate::configure_environment(pkgname)
#' }
#' ```
#'
#' If the Python session has not yet been initialized, or if the user is not
#' using the default Miniconda Python installation, no action will be taken.
#' Otherwise, `reticulate` will take this as a signal to install any required
#' Python dependencies into the user's Python environment.
#'
#' If you'd like to disable `reticulate`'s auto-configure behavior altogether,
#' you can set the environment variable:
#'
#' ```
#' RETICULATE_AUTOCONFIGURE = FALSE
#' ```
#'
#' e.g. in your `~/.Renviron` or similar.
#'
#' Note that, in the case where the Python session has not yet been initialized,
#' `reticulate` will automatically ensure your required Python dependencies
#' are installed after the Python session is initialized (when appropriate).
#'
#' @param package The name of a package to configure. When `NULL`, `reticulate`
#'   will instead look at all loaded packages and discover their associated
#'   Python requirements.
#'
#' @param force Boolean; force configuration of the Python environment? Note
#'   that `configure_environment()` is a no-op within non-interactive \R
#'   sessions. Use this if you require automatic environment configuration, e.g.
#'   when testing a package on a continuous integration service.
#'
#' @export
configure_environment <- function(package = NULL, force = FALSE) {

  # no-op when Python has not yet been initialized
  if (!is_python_initialized())
    return(FALSE)

  # allow opt-out through envvar
  auto <- Sys.getenv("RETICULATE_AUTOCONFIGURE", unset = "TRUE")
  if (auto %in% c("FALSE", "False", "0"))
    return(FALSE)

  # disallow in non-interactive R sessions unless forced
  # (even if force is set, do not allow unless user has explicitly
  # promised they're not on CRAN)
  ok <- interactive() || (force && identical(Sys.getenv("NOT_CRAN"), "true"))
  if (!ok)
    return(FALSE)

  # disallow environment configuration when not using a Python environment
  config <- py_config()
  root <- config$prefix
  ok <- is_virtualenv(root) || is_condaenv(root)
  if (!ok)
    return(FALSE)

  # find Python requirements
  reqs <- python_package_requirements(package)
  if (length(reqs) == 0)
    return(FALSE)

  pkgreqs <- unlist(
    lapply(reqs, `[[`, "packages"),
    recursive = FALSE,
    use.names = FALSE
  )

  # check for incompatible package requests
  df <- do.call(rbind.data.frame, pkgreqs)
  splat <- split(df, df$package)
  pkgreqs <- enumerate(splat, function(pkg, requests) {

    rownames(requests) <- NULL

    # check for explicit requests by version
    explicit <- requests[!is.na(requests$version), ]
    if (nrow(explicit) == 0)
      return(requests[1, ])

    # check for single explicit version request
    n <- length(unique(requests$version))
    if (n == 1)
      return(explicit[1, ])

    # otherwise warn and sort
    explicit <- explicit[order(explicit$version, decreasing = TRUE), ]
    output <- capture.output(format(explicit))

    fmt <- "WARNING: incompatible requirements for package '%s' detected!"
    messagef(fmt, pkg)

    output <- capture.output(format(requests))
    message(paste(output, collapse = "\n"))
    selected <- explicit[1, ]

    fmt <- "WARNING: %s [%s] will be used."
    messagef(fmt, selected$package, selected$version)
    selected

  })

  # split into packages to be installed with pip vs. conda
  # we'll diff the requested packages against the currently-installed
  # packages and only install packages which truly need to be updated
  pip_installed_packages <- NULL
  conda_installed_packages <- NULL

  pip_packages <- character()
  conda_packages <- character()

  for (req in pkgreqs) {

    # if no 'pip' requirement was specified, assume pip
    pip <- req$pip
    if (is.null(pip) || is.na(pip))
      pip <- TRUE

    # if this is a virtual environment, we cannot use conda
    if (nzchar(config$virtualenv %||% ""))
      pip <- TRUE

    # normalize version request
    version <- req$version
    if (is.null(version) || is.na(version))
      version <- NULL


    components <- c(req$package, version)

    if (pip) {

      # read installed packages lazily
      if (is.null(pip_installed_packages)) {
        pip_installed_packages <- pip_freeze(python = config$python)
      }

      # construct requirement string
      requirement <- paste(components, collapse = "==")

      # check to see if we satisfy this requirement already
      satisfied <-
        requirement %in% pip_installed_packages$requirement ||
        requirement %in% pip_installed_packages$package

      if (satisfied)
        next

      pip_packages[[length(pip_packages) + 1]] <- requirement

    } else {

      # read installed packages lazily
      envpath <- dirname(dirname(config$python))
      conda <- conda_binary()

      if (is.null(conda_installed_packages)) {
        conda_installed_packages <- conda_list_packages(
          envname = envpath,
          conda = conda
        )
      }

      # construct requirement string
      requirement <- paste(components, collapse = "=")

      # check to see if we satisfy this requirement already
      satisfied <-
        requirement %in% conda_installed_packages$requirement ||
        requirement %in% conda_installed_packages$package

      if (satisfied)
        next

      conda_packages[[length(conda_packages) + 1]] <- requirement

    }

  }

  if (length(pip_packages) || length(conda_packages)) {

    fmt <- "Configuring package '%s': please wait ..."
    messagef(fmt, package)

    if (length(pip_packages))
      py_install(pip_packages, pip = TRUE)

    if (length(conda_packages))
      py_install(conda_packages, pip = FALSE)

    message("Done!")

  }

  TRUE
}

python_package_requirements <- function(packages = NULL) {

  packages <- packages %||% loadedNamespaces()
  names(packages) <- packages
  reqs <- lapply(packages, function(package) {
    tryCatch(
      python_package_requirements_find(package),
      error = function(e) { warning(e); NULL }
    )
  })

  Filter(Negate(is.null), reqs)

}

python_package_requirements_find <- function(package) {

  descpath <- system.file("DESCRIPTION", package = package)
  desc <- read.dcf(descpath, all = TRUE)

  entry <- desc[["Config/reticulate"]]
  if (is.null(entry))
    return(NULL)

  spec <- eval(parse(text = entry), envir = baseenv())

  fields <- c("package", "version", "pip")
  spec$packages <- lapply(spec$packages, function(req) {

    if (is.null(req$package)) {
      warning("invalid spec provided by package '%s'", package)
      return(NULL)
    }

    data.frame(
      source  = package,
      package = as.character(req[["package"]]),
      version = as.character(req[["version"]] %||% NA),
      pip     = as.logical(req[["pip"]] %||% NA),
      stringsAsFactors = FALSE
    )

  })

  spec

}