mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 13:14:41 +00:00
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:
parent
6ed7302432
commit
9279a125e9
10 changed files with 133 additions and 114 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue