Skip to content

Commit

Permalink
Add a common abstraction to discover PEP 723 script interpreters (#10132
Browse files Browse the repository at this point in the history
)

## Summary

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

(No behavioral changes.)
  • Loading branch information
charliermarsh authored Dec 24, 2024
1 parent 6ed7302 commit 9279a12
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 113 deletions.
1 change: 1 addition & 0 deletions crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ impl PythonInstallation {
&self.interpreter
}

/// Consume the [`PythonInstallation`] and return the [`Interpreter`].
pub fn into_interpreter(self) -> Interpreter {
self.interpreter
}
Expand Down
51 changes: 14 additions & 37 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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};
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -234,7 +211,7 @@ pub(crate) async fn add(
connectivity,
native_tls,
allow_insecure_host,
install_mirrors.clone(),
&install_mirrors,
no_config,
cache,
printer,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ pub(crate) async fn export(
connectivity,
native_tls,
allow_insecure_host,
install_mirrors,
&install_mirrors,
no_config,
cache,
printer,
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ pub(crate) async fn lock(
connectivity,
native_tls,
allow_insecure_host,
install_mirrors,
&install_mirrors,
no_config,
cache,
printer,
Expand Down
128 changes: 99 additions & 29 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<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))
}

/// Consume the [`PythonInstallation`] and return the [`Interpreter`].
pub(crate) fn into_interpreter(self) -> Interpreter {
self.0
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -771,7 +841,7 @@ impl ScriptPython {
pub(crate) async fn get_or_init_environment(
workspace: &Workspace,
python: Option<PythonRequest>,
install_mirrors: PythonInstallMirrors,
install_mirrors: &PythonInstallMirrors,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
connectivity: Connectivity,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 9279a12

Please sign in to comment.