diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index a0b6f9349fe9..2733a14a0885 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -229,6 +229,7 @@ impl PythonInstallation { &self.interpreter } + /// Consume the [`PythonInstallation`] and return the [`Interpreter`]. pub fn into_interpreter(self) -> Interpreter { self.interpreter } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index c0bb80796880..55288defffa7 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -26,10 +26,7 @@ use uv_git::{GitReference, GIT_STORE}; use uv_normalize::{PackageName, DEV_DEPENDENCIES}; use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl}; -use uv_python::{ - EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation, - PythonPreference, PythonRequest, -}; +use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_resolver::{FlatIndex, InstallTarget}; use uv_scripts::{Pep723Item, Pep723Script}; @@ -46,8 +43,7 @@ use crate::commands::pip::loggers::{ use crate::commands::pip::operations::Modifications; use crate::commands::project::lock::LockMode; use crate::commands::project::{ - init_script_python_requirement, lock, validate_script_requires_python, ProjectError, - ProjectInterpreter, ScriptPython, + init_script_python_requirement, lock, ProjectError, ProjectInterpreter, ScriptInterpreter, }; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{diagnostics, project, ExitStatus}; @@ -144,7 +140,7 @@ pub(crate) async fn add( } else { let requires_python = init_script_python_requirement( python.as_deref(), - install_mirrors.clone(), + &install_mirrors, project_dir, false, python_preference, @@ -158,42 +154,23 @@ pub(crate) async fn add( Pep723Script::init(&script, requires_python.specifiers()).await? }; - let ScriptPython { - source, - python_request, - requires_python, - } = ScriptPython::from_request( - python.as_deref().map(PythonRequest::parse), - None, + // Discover the interpreter. + let interpreter = ScriptInterpreter::discover( &Pep723Item::Script(script.clone()), - no_config, - ) - .await?; - - let interpreter = PythonInstallation::find_or_download( - python_request.as_ref(), - EnvironmentPreference::Any, + python.as_deref().map(PythonRequest::parse), python_preference, python_downloads, - &client_builder, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, cache, - Some(&reporter), - install_mirrors.python_install_mirror.as_deref(), - install_mirrors.pypy_install_mirror.as_deref(), + printer, ) .await? .into_interpreter(); - if let Some((requires_python, requires_python_source)) = requires_python { - validate_script_requires_python( - &interpreter, - None, - &requires_python, - &requires_python_source, - &source, - )?; - } - Target::Script(script, Box::new(interpreter)) } else { // Find the project in the workspace. @@ -234,7 +211,7 @@ pub(crate) async fn add( connectivity, native_tls, allow_insecure_host, - install_mirrors.clone(), + &install_mirrors, no_config, cache, printer, @@ -248,7 +225,7 @@ pub(crate) async fn add( let venv = project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), - install_mirrors.clone(), + &install_mirrors, python_preference, python_downloads, connectivity, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 4368af06cf84..00ed04bf5207 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -114,7 +114,7 @@ pub(crate) async fn export( connectivity, native_tls, allow_insecure_host, - install_mirrors, + &install_mirrors, no_config, cache, printer, diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 8974dcc5214e..18f2b587b9d7 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -242,7 +242,7 @@ async fn init_script( let requires_python = init_script_python_requirement( python.as_deref(), - install_mirrors, + &install_mirrors, &CWD, no_pin_python, python_preference, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index cf36c8e6349e..59c73e59241c 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -109,7 +109,7 @@ pub(crate) async fn lock( connectivity, native_tls, allow_insecure_host, - install_mirrors, + &install_mirrors, no_config, cache, printer, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index e0e7f4b0d24b..e9243651652a 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -397,7 +397,7 @@ pub(crate) fn validate_requires_python( /// Returns an error if the [`Interpreter`] does not satisfy script or workspace `requires-python`. #[allow(clippy::result_large_err)] -pub(crate) fn validate_script_requires_python( +fn validate_script_requires_python( interpreter: &Interpreter, workspace: Option<&Workspace>, requires_python: &RequiresPython, @@ -406,35 +406,105 @@ pub(crate) fn validate_script_requires_python( ) -> Result<(), ProjectError> { match requires_python_source { RequiresPythonSource::Project => { - validate_requires_python(interpreter, workspace, requires_python, request_source)?; + validate_requires_python(interpreter, workspace, requires_python, request_source) } - RequiresPythonSource::Script => {} - }; + RequiresPythonSource::Script => { + if requires_python.contains(interpreter.python_version()) { + return Ok(()); + } - if requires_python.contains(interpreter.python_version()) { - return Ok(()); + match request_source { + PythonRequestSource::UserRequest => { + Err(ProjectError::RequestedPythonScriptIncompatibility( + interpreter.python_version().clone(), + requires_python.clone(), + )) + } + PythonRequestSource::DotPythonVersion(file) => { + Err(ProjectError::DotPythonVersionScriptIncompatibility( + file.file_name().to_string(), + interpreter.python_version().clone(), + requires_python.clone(), + )) + } + PythonRequestSource::RequiresPython => { + Err(ProjectError::RequiresPythonScriptIncompatibility( + interpreter.python_version().clone(), + requires_python.clone(), + )) + } + } + } } +} - match request_source { - PythonRequestSource::UserRequest => { - Err(ProjectError::RequestedPythonScriptIncompatibility( - interpreter.python_version().clone(), - requires_python.clone(), - )) - } - PythonRequestSource::DotPythonVersion(file) => { - Err(ProjectError::DotPythonVersionScriptIncompatibility( - file.file_name().to_string(), - interpreter.python_version().clone(), - requires_python.clone(), - )) - } - PythonRequestSource::RequiresPython => { - Err(ProjectError::RequiresPythonScriptIncompatibility( - interpreter.python_version().clone(), - requires_python.clone(), - )) +/// An interpreter suitable for a PEP 723 script. +#[derive(Debug, Clone)] +pub(crate) struct ScriptInterpreter(Interpreter); + +impl ScriptInterpreter { + /// Discover the interpreter to use for the current [`Pep723Item`]. + pub(crate) async fn discover( + script: &Pep723Item, + python_request: Option, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + connectivity: Connectivity, + native_tls: bool, + allow_insecure_host: &[TrustedHost], + install_mirrors: &PythonInstallMirrors, + no_config: bool, + cache: &Cache, + printer: Printer, + ) -> Result { + // For now, we assume that scripts are never evaluated in the context of a workspace. + let workspace = None; + + let ScriptPython { + source, + python_request, + requires_python, + } = ScriptPython::from_request(python_request, workspace, script, no_config).await?; + + let client_builder = BaseClientBuilder::new() + .connectivity(connectivity) + .native_tls(native_tls) + .allow_insecure_host(allow_insecure_host.to_vec()); + + let reporter = PythonDownloadReporter::single(printer); + + let interpreter = PythonInstallation::find_or_download( + python_request.as_ref(), + EnvironmentPreference::Any, + python_preference, + python_downloads, + &client_builder, + cache, + Some(&reporter), + install_mirrors.python_install_mirror.as_deref(), + install_mirrors.pypy_install_mirror.as_deref(), + ) + .await? + .into_interpreter(); + + if let Some((requires_python, requires_python_source)) = requires_python { + if let Err(err) = validate_script_requires_python( + &interpreter, + workspace, + &requires_python, + &requires_python_source, + &source, + ) { + warn_user!("{err}"); + } } + + Ok(Self(interpreter)) + } + + /// Consume the [`PythonInstallation`] and return the [`Interpreter`]. + pub(crate) fn into_interpreter(self) -> Interpreter { + self.0 } } @@ -459,7 +529,7 @@ impl ProjectInterpreter { connectivity: Connectivity, native_tls: bool, allow_insecure_host: &[TrustedHost], - install_mirrors: PythonInstallMirrors, + install_mirrors: &PythonInstallMirrors, no_config: bool, cache: &Cache, printer: Printer, @@ -547,7 +617,7 @@ impl ProjectInterpreter { let reporter = PythonDownloadReporter::single(printer); - // Locate the Python interpreter to use in the environment + // Locate the Python interpreter to use in the environment. let python = PythonInstallation::find_or_download( python_request.as_ref(), EnvironmentPreference::OnlySystem, @@ -771,7 +841,7 @@ impl ScriptPython { pub(crate) async fn get_or_init_environment( workspace: &Workspace, python: Option, - install_mirrors: PythonInstallMirrors, + install_mirrors: &PythonInstallMirrors, python_preference: PythonPreference, python_downloads: PythonDownloads, connectivity: Connectivity, @@ -1598,7 +1668,7 @@ pub(crate) async fn update_environment( /// Determine the [`RequiresPython`] requirement for a new PEP 723 script. pub(crate) async fn init_script_python_requirement( python: Option<&str>, - install_mirrors: PythonInstallMirrors, + install_mirrors: &PythonInstallMirrors, directory: &Path, no_pin_python: bool, python_preference: PythonPreference, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index effa0752e205..df9e8ee9228a 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -194,7 +194,7 @@ pub(crate) async fn remove( let venv = project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), - install_mirrors, + &install_mirrors, python_preference, python_downloads, connectivity, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index cb51e7c8264f..4c68751c476f 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -45,8 +45,8 @@ use crate::commands::pip::operations::Modifications; use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::lock::LockMode; use crate::commands::project::{ - default_dependency_groups, validate_requires_python, validate_script_requires_python, - DependencyGroupsTarget, EnvironmentSpecification, ProjectError, ScriptPython, WorkspacePython, + default_dependency_groups, validate_requires_python, DependencyGroupsTarget, + EnvironmentSpecification, ProjectError, ScriptInterpreter, WorkspacePython, }; use crate::commands::reporters::PythonDownloadReporter; use crate::commands::{diagnostics, project, ExitStatus}; @@ -181,52 +181,23 @@ pub(crate) async fn run( } } - let ScriptPython { - source, - python_request, - requires_python, - } = ScriptPython::from_request( - python.as_deref().map(PythonRequest::parse), - None, + // Discover the interpreter for the script. + let interpreter = ScriptInterpreter::discover( &script, - no_config, - ) - .await?; - - let client_builder = BaseClientBuilder::new() - .connectivity(connectivity) - .native_tls(native_tls) - .allow_insecure_host(allow_insecure_host.to_vec()); - - let interpreter = PythonInstallation::find_or_download( - python_request.as_ref(), - EnvironmentPreference::Any, + python.as_deref().map(PythonRequest::parse), python_preference, python_downloads, - &client_builder, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, cache, - Some(&download_reporter), - install_mirrors.python_install_mirror.as_deref(), - install_mirrors.pypy_install_mirror.as_deref(), + printer, ) .await? .into_interpreter(); - if let Some((requires_python, requires_python_source)) = requires_python { - match validate_script_requires_python( - &interpreter, - None, - &requires_python, - &requires_python_source, - &source, - ) { - Ok(()) => {} - Err(err) => { - warn_user!("{err}"); - } - } - } - // Determine the working directory for the script. let script_dir = match &script { Pep723Item::Script(script) => std::path::absolute(&script.path)? @@ -592,7 +563,7 @@ pub(crate) async fn run( project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), - install_mirrors, + &install_mirrors, python_preference, python_downloads, connectivity, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 935e7ddef2c5..53fd3a573359 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -120,7 +120,7 @@ pub(crate) async fn sync( let venv = project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), - install_mirrors, + &install_mirrors, python_preference, python_downloads, connectivity, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index b62ef2b54801..34226bf93c07 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -86,7 +86,7 @@ pub(crate) async fn tree( connectivity, native_tls, allow_insecure_host, - install_mirrors, + &install_mirrors, no_config, cache, printer,