@@ -147,7 +147,7 @@ print.python_requirements <- function(x, ...) {
147
147
}
148
148
python_version <- x $ python_version
149
149
if (is.null(python_version )) {
150
- python_version <- paste0(" [No Python version specified. Will default to '" , reticulate_default_python () , " ']" )
150
+ python_version <- paste0(" [No Python version specified. Will default to '" , resolve_python_version () , " ']" )
151
151
}
152
152
153
153
requested_from <- as.character(lapply(x $ history , function (x ) x $ requested_from ))
@@ -436,20 +436,14 @@ uv_get_or_create_env <- function(packages = py_reqs_get("packages"),
436
436
call_args <- list (
437
437
packages = packages ,
438
438
python_version = python_version %|| %
439
- paste(reticulate_default_python (), " (reticulate default)" ),
439
+ paste(resolve_python_version (), " (reticulate default)" ),
440
440
exclude_newer = exclude_newer
441
441
)
442
442
443
443
if (length(packages ))
444
444
packages <- as.vector(rbind(" --with" , packages ))
445
445
446
- if (length(python_version )) {
447
- constraints <- unlist(strsplit(python_version , " ," , fixed = TRUE ))
448
- constraints <- paste0(constraints , collapse = " ," )
449
- } else {
450
- constraints <- reticulate_default_python()
451
- }
452
- python_version <- c(" --python" , constraints )
446
+ python_version <- c(" --python" , resolve_python_version(constraints = python_version ))
453
447
454
448
if (! is.null(exclude_newer )) {
455
449
# todo, accept a POSIXct/lt, format correctly
@@ -462,7 +456,7 @@ uv_get_or_create_env <- function(packages = py_reqs_get("packages"),
462
456
c(" --cache-dir" , reticulate_managed_uv_cache_dir())
463
457
464
458
if (is_positron())
465
- withr :: local_envvar(c(RUST_LOG = NA ))
459
+ withr :: local_envvar(c(RUST_LOG = NA ))
466
460
467
461
uv_args <- c(
468
462
" run" ,
@@ -482,32 +476,30 @@ uv_get_or_create_env <- function(packages = py_reqs_get("packages"),
482
476
# This is an interactive session, but uv will not display any progress bar
483
477
# during long downloads because isatty() == FALSE. So we use processx and
484
478
# cli to display a calming spinner.
485
- p <- processx :: process $ new(
486
- uv , uv_args ,
487
- stderr = " |" ,
488
- stdout = " |"
489
- )
490
- uv_stderr <- character ()
479
+ p <- processx :: process $ new(uv , uv_args , stderr = " |" , stdout = " |" )
480
+ uv_stdout <- uv_stderr <- character ()
491
481
p $ wait(500 )
492
482
if (p $ is_alive()) {
493
483
sp <- cli :: make_spinner(template = " Downloading Python dependencies {spin}" )
494
484
while (p $ is_alive()) {
495
485
sp $ spin()
496
486
pr <- p $ poll_io(100 )
497
- if (pr [[" error" ]] == " ready" ) {
498
- # proactively clear pipe to ensure process isn't blocked on write
487
+ if (pr [[" error" ]] == " ready" )
499
488
uv_stderr [[length(uv_stderr ) + 1L ]] <- p $ read_error()
500
- }
489
+ if (pr [[" output" ]] == " ready" )
490
+ uv_stdout [[length(uv_stdout ) + 1L ]] <- p $ read_output()
501
491
}
502
492
sp $ finish()
503
493
}
504
494
505
495
uv_stderr <- paste0(c(uv_stderr , p $ read_all_error()), collapse = " " )
496
+ uv_stdout <- paste0(c(uv_stdout , p $ read_all_output()), collapse = " " )
497
+
506
498
if (nzchar(uv_stderr ))
507
499
cat(uv_stderr , if (! endsWith(uv_stderr , " \n " )) " \n " , file = stderr())
508
500
509
501
exit_status <- p $ get_exit_status()
510
- env_python <- sub(" [\r\n ]*$" , " " , p $ read_all_output() )
502
+ env_python <- sub(" [\r\n ]*$" , " " , uv_stdout )
511
503
512
504
} else {
513
505
# uv will display a progress bar during long downloads, and we can simply
@@ -578,3 +570,73 @@ uv_cache_dir <- function(uv = uv_binary(bootstrap_install = FALSE)) {
578
570
error = function (e ) NULL
579
571
)
580
572
}
573
+
574
+
575
+ uv_python_list <- function () {
576
+ x <- system2(uv_binary(), c(
577
+ " python list --no-config --python-preference only-managed" ,
578
+ " --only-downloads --color never"
579
+ ),
580
+ stdout = TRUE
581
+ )
582
+
583
+ x <- grep(" ^cpython-" , x , value = TRUE )
584
+ x <- sub(" ^cpython-([^-]+)-.*" , " \\ 1" , x )
585
+ x <- numeric_version(x , strict = FALSE )
586
+ x <- x [! is.na(x )]
587
+ x <- sort(x , decreasing = TRUE )
588
+ x
589
+ }
590
+
591
+ resolve_python_version <- function (constraints = NULL ) {
592
+ constraints <- as.character(constraints %|| % " " )
593
+ constraints <- trimws(unlist(strsplit(constraints , " ," , fixed = TRUE )))
594
+ constraints <- constraints [nzchar(constraints )]
595
+
596
+ if (length(constraints ) == 0 ) {
597
+ return (as.character(uv_python_list()[3L ])) # default
598
+ }
599
+
600
+ # reflect a direct version specification like "3.11" or "3.14.0a3"
601
+ if (length(constraints ) == 1 && ! substr(constraints , 1 , 1 ) %in% c(" =" , " >" , " <" , " !" )) {
602
+ return (constraints )
603
+ }
604
+
605
+ # We perform custom constraint resolution to prefer slightly older Python releases.
606
+ # uv tends to select the latest version, which often lack package support
607
+ # See: https://devguide.python.org/versions/
608
+
609
+ # Get latest patch for each minor version
610
+ candidates <- uv_python_list()
611
+ # E.g., candidates might be:
612
+ # c("3.13.1", "3.12.8", "3.11.11", "3.10.16", "3.9.21", "3.8.20")
613
+
614
+ # Reorder candidates to prefer stable versions over bleeding edge
615
+ ord <- as.integer(c(3 , 4 , 2 , 5 , 1 ))
616
+ ord <- union(ord , seq_along(candidates ))
617
+ candidates <- candidates [ord ]
618
+
619
+ # Maybe add non-latest patch levels to candidates if they're explicitly
620
+ # mentioned in constraints
621
+ additional_candidates <- sub(" ^[<>=!]{1,2}" , " " , constraints )
622
+ additional_candidates <- numeric_version(additional_candidates , strict = FALSE )
623
+ additional_candidates <- additional_candidates [! is.na(additional_candidates )]
624
+ candidates <- c(candidates , additional_candidates )
625
+
626
+ for (check in as_version_constraint_checkers(constraints )) {
627
+ satisfies_constraint <- check(candidates )
628
+ candidates <- candidates [satisfies_constraint ]
629
+ }
630
+
631
+ if (! length(candidates )) {
632
+ constraints <- paste0(constraints , collapse = " ," )
633
+ msg <- paste0(
634
+ ' Requested Python version constraints could not be satisfied.\n ' ,
635
+ ' constraints: "' , constraints , ' "\n ' ,
636
+ ' Hint: Call `py_require(python_version = <string>, action = "set")` to replace constraints.'
637
+ )
638
+ stop(msg )
639
+ }
640
+
641
+ as.character(candidates [1L ])
642
+ }
0 commit comments