@@ -291,10 +291,126 @@ initialize_python <- function(required_module = NULL, use_environment = NULL) {
291291 }
292292 }
293293
294+ if (nzchar(config $ virtualenv )) {
295+ check_required_packages <- Sys.getenv(" RETICULATE_CHECK_REQUIRED_PACKAGES" , " true" )
296+ check_required_packages <- tolower(check_required_packages ) %in% c(" true" , " 1" , " yes" )
297+ if (check_required_packages ) {
298+ tryCatch(check_virtualenv_required_packages(config ), error = function (e ) {
299+ # ignore errors, this should never block initialization
300+ })
301+ }
302+ }
303+
294304 # return config
295305 config
296306}
297307
308+ # ' @returns Invisibly returns TRUE if all required packages are installed.
309+ # ' It returns `NULL` if it is unable to perform the check.
310+ # ' Returns `FALSE` if required packages are missing, but also issues warnings
311+ # ' containing more details about the missing packages.
312+ check_virtualenv_required_packages <- function (config ) {
313+
314+ # TODO: also check py_require()$python_version.
315+ # TODO: also warn if py_require() is called after reticulate has initialize Python.
316+
317+ # we don't install uv if it's not yet installed
318+ uv <- uv_binary(bootstrap_install = FALSE )
319+ if (is.null(uv )) {
320+ return (invisible (NULL ))
321+ }
322+ # force uv to use the selected python binary.
323+ # in principle this could be ommited assuming that VIRTUAL_ENV is set
324+
325+ packages <- py_require()$ packages
326+ if (length(packages ) == 0 ) {
327+ return (invisible (TRUE ))
328+ }
329+
330+ args <- c(" pip install --dry-run --no-deps" ,
331+ " --color never --no-progress" ,
332+ " --no-build" ,
333+ # "--verbose",
334+ # "--offline", # we don't set offline because it blocks env resolution
335+ " --no-config" ,
336+ " --python" , maybe_shQuote(config $ python ),
337+ maybe_shQuote(packages ))
338+
339+ suppressWarnings({
340+ pip_output <- uv_exec(args , stdout = TRUE , stderr = TRUE )
341+ })
342+
343+ status <- attr(pip_output , " status" ) %|| % 0L
344+ if (status != 0L ) {
345+ # check failed.
346+ # there are a few possible reasons:
347+ # 1) a required package is not available in the index.
348+ # No solution found when resolving dependencies:
349+ # ╰─▶ Because <pkg> was not found in the package registry and you require <pkg>,
350+ # we can conclude that your requirements are unsatisfiable.
351+ # 2) Any failure to access the index (timeouts, network issues, etc)
352+ # same error as above. Since there's no way to differentiate these cases,
353+ # we just return NULL.
354+ return (invisible (NULL ))
355+ }
356+
357+ # we don't want this to trigger package downloads. we forward
358+ # downloading lines to warnings so that we can detect if a download
359+ # happened on CI.
360+ # In case uv downloads a package with the --no-progress option it would show something
361+ # like:
362+ # Resolved 3 packages in 2ms
363+ # Downloading numba (2.6MiB) <-
364+ # Downloading numba <-
365+ # Prepared 1 package in 579ms
366+ # Uninstalled 2 packages in 61ms
367+ # Installed 2 packages in 11ms
368+ # - llvmlite==0.43.0
369+ # + llvmlite==0.44.0
370+ # - numba==0.60.0
371+ # + numba==0.61.0
372+ if (any(grepl(" downloading" , pip_output , ignore.case = TRUE ))) {
373+ warning(paste(
374+ pip_output [grepl(" downloading" , pip_output , ignore.case = TRUE )],
375+ collapse = " \n "
376+ ))
377+ }
378+
379+ # uv pip doesn't have an option to produce structured output,
380+ # the best we can do is parse using a regex.
381+ # the output contains lines like:
382+ # > Would install <n> packages
383+ # we use that to determine if any packages would be installed
384+ pattern <- " Would install (\\ d+) package(s)?"
385+ would_install <- any(grepl(pattern , pip_output ))
386+
387+ if (would_install ) {
388+ # subset the py_require()$packages vector to keep only those that
389+ # match output from `uv pip install`.
390+
391+ # keep lines that list package versions like " + docutils==1.2.3"
392+ would_install_pkgs <- grep(" ==" , pip_output , fixed = TRUE , value = TRUE )
393+
394+ # extract just the package name like "docutils"
395+ would_install_pkgs <- sub(" (...)([^=]+)(==.*)" , " \\ 2" , would_install_pkgs )
396+
397+ # subset packages to keep only entries found in `uv pip` output.
398+ would_install_pkgs <- packages [
399+ vapply(packages , function (pkg ) any(startsWith(pkg , would_install_pkgs )), TRUE ,
400+ USE.NAMES = FALSE )
401+ ]
402+ warning(
403+ " Some Python package requirements declared via `py_require()` are not" ,
404+ " installed in the selected Python environment: (" , config $ python , " )\n " ,
405+ " " , paste0(would_install_pkgs , collapse = " " ),
406+ call. = FALSE
407+ )
408+ return (invisible (FALSE ))
409+ }
410+
411+ invisible (TRUE )
412+ }
413+
298414# unused presently, formerly called from initialize_python()
299415# https://github.com/rstudio/reticulate/commit/e8c82a1f95eb97c4e5fc27b6550a4498827438e0#r122213856
300416check_forbidden_initialization <- function () {
0 commit comments