Add a common abstraction to discover PEP 723 script interpreters (#10132)

## Summary

This logic is already repeated twice, and I'm on the verge of adding a
third.

(No behavioral changes.)
This commit is contained in:
Charlie Marsh 2024-12-23 19:29:37 -05:00 committed by GitHub
parent 6ed7302432
commit 9279a125e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 133 additions and 114 deletions

View file

@ -229,6 +229,7 @@ impl PythonInstallation {
&self.interpreter &self.interpreter
} }
/// Consume the [`PythonInstallation`] and return the [`Interpreter`].
pub fn into_interpreter(self) -> Interpreter { pub fn into_interpreter(self) -> Interpreter {
self.interpreter self.interpreter
} }

View file

@ -26,10 +26,7 @@ use uv_git::{GitReference, GIT_STORE};
use uv_normalize::{PackageName, DEV_DEPENDENCIES}; use uv_normalize::{PackageName, DEV_DEPENDENCIES};
use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl}; use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl};
use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl}; use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl};
use uv_python::{ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest,
};
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
use uv_resolver::{FlatIndex, InstallTarget}; use uv_resolver::{FlatIndex, InstallTarget};
use uv_scripts::{Pep723Item, Pep723Script}; use uv_scripts::{Pep723Item, Pep723Script};
@ -46,8 +43,7 @@ use crate::commands::pip::loggers::{
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::project::lock::LockMode; use crate::commands::project::lock::LockMode;
use crate::commands::project::{ use crate::commands::project::{
init_script_python_requirement, lock, validate_script_requires_python, ProjectError, init_script_python_requirement, lock, ProjectError, ProjectInterpreter, ScriptInterpreter,
ProjectInterpreter, ScriptPython,
}; };
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{diagnostics, project, ExitStatus}; use crate::commands::{diagnostics, project, ExitStatus};
@ -144,7 +140,7 @@ pub(crate) async fn add(
} else { } else {
let requires_python = init_script_python_requirement( let requires_python = init_script_python_requirement(
python.as_deref(), python.as_deref(),
install_mirrors.clone(), &install_mirrors,
project_dir, project_dir,
false, false,
python_preference, python_preference,
@ -158,42 +154,23 @@ pub(crate) async fn add(
Pep723Script::init(&script, requires_python.specifiers()).await? Pep723Script::init(&script, requires_python.specifiers()).await?
}; };
let ScriptPython { // Discover the interpreter.
source, let interpreter = ScriptInterpreter::discover(
python_request,
requires_python,
} = ScriptPython::from_request(
python.as_deref().map(PythonRequest::parse),
None,
&Pep723Item::Script(script.clone()), &Pep723Item::Script(script.clone()),
no_config, python.as_deref().map(PythonRequest::parse),
)
.await?;
let interpreter = PythonInstallation::find_or_download(
python_request.as_ref(),
EnvironmentPreference::Any,
python_preference, python_preference,
python_downloads, python_downloads,
&client_builder, connectivity,
native_tls,
allow_insecure_host,
&install_mirrors,
no_config,
cache, cache,
Some(&reporter), printer,
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
) )
.await? .await?
.into_interpreter(); .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)) Target::Script(script, Box::new(interpreter))
} else { } else {
// Find the project in the workspace. // Find the project in the workspace.
@ -234,7 +211,7 @@ pub(crate) async fn add(
connectivity, connectivity,
native_tls, native_tls,
allow_insecure_host, allow_insecure_host,
install_mirrors.clone(), &install_mirrors,
no_config, no_config,
cache, cache,
printer, printer,
@ -248,7 +225,7 @@ pub(crate) async fn add(
let venv = project::get_or_init_environment( let venv = project::get_or_init_environment(
project.workspace(), project.workspace(),
python.as_deref().map(PythonRequest::parse), python.as_deref().map(PythonRequest::parse),
install_mirrors.clone(), &install_mirrors,
python_preference, python_preference,
python_downloads, python_downloads,
connectivity, connectivity,

View file

@ -114,7 +114,7 @@ pub(crate) async fn export(
connectivity, connectivity,
native_tls, native_tls,
allow_insecure_host, allow_insecure_host,
install_mirrors, &install_mirrors,
no_config, no_config,
cache, cache,
printer, printer,

View file

@ -242,7 +242,7 @@ async fn init_script(
let requires_python = init_script_python_requirement( let requires_python = init_script_python_requirement(
python.as_deref(), python.as_deref(),
install_mirrors, &install_mirrors,
&CWD, &CWD,
no_pin_python, no_pin_python,
python_preference, python_preference,

View file

@ -109,7 +109,7 @@ pub(crate) async fn lock(
connectivity, connectivity,
native_tls, native_tls,
allow_insecure_host, allow_insecure_host,
install_mirrors, &install_mirrors,
no_config, no_config,
cache, cache,
printer, printer,

View file

@ -397,7 +397,7 @@ pub(crate) fn validate_requires_python(
/// Returns an error if the [`Interpreter`] does not satisfy script or workspace `requires-python`. /// Returns an error if the [`Interpreter`] does not satisfy script or workspace `requires-python`.
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub(crate) fn validate_script_requires_python( fn validate_script_requires_python(
interpreter: &Interpreter, interpreter: &Interpreter,
workspace: Option<&Workspace>, workspace: Option<&Workspace>,
requires_python: &RequiresPython, requires_python: &RequiresPython,
@ -406,35 +406,105 @@ pub(crate) fn validate_script_requires_python(
) -> Result<(), ProjectError> { ) -> Result<(), ProjectError> {
match requires_python_source { match requires_python_source {
RequiresPythonSource::Project => { 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()) { match request_source {
return Ok(()); 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<PythonRequest>,
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<Self, ProjectError> {
// 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))
} }
match request_source { /// Consume the [`PythonInstallation`] and return the [`Interpreter`].
PythonRequestSource::UserRequest => { pub(crate) fn into_interpreter(self) -> Interpreter {
Err(ProjectError::RequestedPythonScriptIncompatibility( self.0
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(),
))
}
} }
} }
@ -459,7 +529,7 @@ impl ProjectInterpreter {
connectivity: Connectivity, connectivity: Connectivity,
native_tls: bool, native_tls: bool,
allow_insecure_host: &[TrustedHost], allow_insecure_host: &[TrustedHost],
install_mirrors: PythonInstallMirrors, install_mirrors: &PythonInstallMirrors,
no_config: bool, no_config: bool,
cache: &Cache, cache: &Cache,
printer: Printer, printer: Printer,
@ -547,7 +617,7 @@ impl ProjectInterpreter {
let reporter = PythonDownloadReporter::single(printer); 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( let python = PythonInstallation::find_or_download(
python_request.as_ref(), python_request.as_ref(),
EnvironmentPreference::OnlySystem, EnvironmentPreference::OnlySystem,
@ -771,7 +841,7 @@ impl ScriptPython {
pub(crate) async fn get_or_init_environment( pub(crate) async fn get_or_init_environment(
workspace: &Workspace, workspace: &Workspace,
python: Option<PythonRequest>, python: Option<PythonRequest>,
install_mirrors: PythonInstallMirrors, install_mirrors: &PythonInstallMirrors,
python_preference: PythonPreference, python_preference: PythonPreference,
python_downloads: PythonDownloads, python_downloads: PythonDownloads,
connectivity: Connectivity, connectivity: Connectivity,
@ -1598,7 +1668,7 @@ pub(crate) async fn update_environment(
/// Determine the [`RequiresPython`] requirement for a new PEP 723 script. /// Determine the [`RequiresPython`] requirement for a new PEP 723 script.
pub(crate) async fn init_script_python_requirement( pub(crate) async fn init_script_python_requirement(
python: Option<&str>, python: Option<&str>,
install_mirrors: PythonInstallMirrors, install_mirrors: &PythonInstallMirrors,
directory: &Path, directory: &Path,
no_pin_python: bool, no_pin_python: bool,
python_preference: PythonPreference, python_preference: PythonPreference,

View file

@ -194,7 +194,7 @@ pub(crate) async fn remove(
let venv = project::get_or_init_environment( let venv = project::get_or_init_environment(
project.workspace(), project.workspace(),
python.as_deref().map(PythonRequest::parse), python.as_deref().map(PythonRequest::parse),
install_mirrors, &install_mirrors,
python_preference, python_preference,
python_downloads, python_downloads,
connectivity, connectivity,

View file

@ -45,8 +45,8 @@ use crate::commands::pip::operations::Modifications;
use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::environment::CachedEnvironment;
use crate::commands::project::lock::LockMode; use crate::commands::project::lock::LockMode;
use crate::commands::project::{ use crate::commands::project::{
default_dependency_groups, validate_requires_python, validate_script_requires_python, default_dependency_groups, validate_requires_python, DependencyGroupsTarget,
DependencyGroupsTarget, EnvironmentSpecification, ProjectError, ScriptPython, WorkspacePython, EnvironmentSpecification, ProjectError, ScriptInterpreter, WorkspacePython,
}; };
use crate::commands::reporters::PythonDownloadReporter; use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{diagnostics, project, ExitStatus}; use crate::commands::{diagnostics, project, ExitStatus};
@ -181,52 +181,23 @@ pub(crate) async fn run(
} }
} }
let ScriptPython { // Discover the interpreter for the script.
source, let interpreter = ScriptInterpreter::discover(
python_request,
requires_python,
} = ScriptPython::from_request(
python.as_deref().map(PythonRequest::parse),
None,
&script, &script,
no_config, python.as_deref().map(PythonRequest::parse),
)
.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_preference, python_preference,
python_downloads, python_downloads,
&client_builder, connectivity,
native_tls,
allow_insecure_host,
&install_mirrors,
no_config,
cache, cache,
Some(&download_reporter), printer,
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
) )
.await? .await?
.into_interpreter(); .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. // Determine the working directory for the script.
let script_dir = match &script { let script_dir = match &script {
Pep723Item::Script(script) => std::path::absolute(&script.path)? Pep723Item::Script(script) => std::path::absolute(&script.path)?
@ -592,7 +563,7 @@ pub(crate) async fn run(
project::get_or_init_environment( project::get_or_init_environment(
project.workspace(), project.workspace(),
python.as_deref().map(PythonRequest::parse), python.as_deref().map(PythonRequest::parse),
install_mirrors, &install_mirrors,
python_preference, python_preference,
python_downloads, python_downloads,
connectivity, connectivity,

View file

@ -120,7 +120,7 @@ pub(crate) async fn sync(
let venv = project::get_or_init_environment( let venv = project::get_or_init_environment(
project.workspace(), project.workspace(),
python.as_deref().map(PythonRequest::parse), python.as_deref().map(PythonRequest::parse),
install_mirrors, &install_mirrors,
python_preference, python_preference,
python_downloads, python_downloads,
connectivity, connectivity,

View file

@ -86,7 +86,7 @@ pub(crate) async fn tree(
connectivity, connectivity,
native_tls, native_tls,
allow_insecure_host, allow_insecure_host,
install_mirrors, &install_mirrors,
no_config, no_config,
cache, cache,
printer, printer,