-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for uvx python
#11076
Add support for uvx python
#11076
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,12 +15,14 @@ use uv_cache_info::Timestamp; | |
use uv_cli::ExternalCommand; | ||
use uv_client::{BaseClientBuilder, Connectivity}; | ||
use uv_configuration::{Concurrency, PreviewMode, TrustedHost}; | ||
use uv_distribution_types::UnresolvedRequirement; | ||
use uv_distribution_types::{Name, UnresolvedRequirementSpecification}; | ||
use uv_installer::{SatisfiesResult, SitePackages}; | ||
use uv_normalize::PackageName; | ||
use uv_pep440::{VersionSpecifier, VersionSpecifiers}; | ||
use uv_pep508::MarkerTree; | ||
use uv_pypi_types::{Requirement, RequirementSource}; | ||
use uv_python::VersionRequest; | ||
use uv_python::{ | ||
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation, | ||
PythonPreference, PythonRequest, | ||
|
@@ -183,12 +185,17 @@ pub(crate) async fn run( | |
|
||
// We check if the provided command is not part of the executables for the `from` package. | ||
// If the command is found in other packages, we warn the user about the correct package to use. | ||
warn_executable_not_provided_by_package( | ||
executable, | ||
&from.name, | ||
&site_packages, | ||
invocation_source, | ||
); | ||
match &from { | ||
ToolRequirement::Python => {} | ||
ToolRequirement::Package(from) => { | ||
warn_executable_not_provided_by_package( | ||
executable, | ||
&from.name, | ||
&site_packages, | ||
invocation_source, | ||
); | ||
} | ||
} | ||
|
||
let handle = match process.spawn() { | ||
Ok(handle) => Ok(handle), | ||
|
@@ -216,11 +223,15 @@ pub(crate) async fn run( | |
/// Returns an exit status if the caller should exit after hinting. | ||
fn hint_on_not_found( | ||
executable: &str, | ||
from: &Requirement, | ||
from: &ToolRequirement, | ||
site_packages: &SitePackages, | ||
invocation_source: ToolRunCommand, | ||
printer: Printer, | ||
) -> anyhow::Result<Option<ExitStatus>> { | ||
let from = match from { | ||
ToolRequirement::Python => return Ok(None), | ||
ToolRequirement::Package(from) => from, | ||
}; | ||
match get_entrypoints(&from.name, site_packages) { | ||
Ok(entrypoints) => { | ||
writeln!( | ||
|
@@ -397,6 +408,23 @@ fn warn_executable_not_provided_by_package( | |
} | ||
} | ||
|
||
// Clippy isn't happy about the difference in size between these variants, but | ||
// [`ToolRequirement::Package`] is the more common case and it seems annoying to box it. | ||
#[allow(clippy::large_enum_variant)] | ||
pub(crate) enum ToolRequirement { | ||
Python, | ||
Package(Requirement), | ||
} | ||
|
||
impl std::fmt::Display for ToolRequirement { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
ToolRequirement::Python => write!(f, "python"), | ||
ToolRequirement::Package(requirement) => write!(f, "{requirement}"), | ||
} | ||
} | ||
} | ||
|
||
/// Get or create a [`PythonEnvironment`] in which to run the specified tools. | ||
/// | ||
/// If the target tool is already installed in a compatible environment, returns that | ||
|
@@ -420,15 +448,52 @@ async fn get_or_create_environment( | |
cache: &Cache, | ||
printer: Printer, | ||
preview: PreviewMode, | ||
) -> Result<(Requirement, PythonEnvironment), ProjectError> { | ||
) -> Result<(ToolRequirement, PythonEnvironment), ProjectError> { | ||
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 python_request = python.map(PythonRequest::parse); | ||
// Check if the target is `python` | ||
let python_request = if target.is_python() { | ||
let target_request = match target { | ||
Target::Unspecified(_) => None, | ||
Target::Version(_, version) | Target::FromVersion(_, _, version) => { | ||
Some(PythonRequest::Version( | ||
VersionRequest::from_str(&version.to_string()).map_err(anyhow::Error::from)?, | ||
)) | ||
} | ||
// TODO(zanieb): Add `PythonRequest::Latest` | ||
Target::Latest(_) | Target::FromLatest(_, _) => { | ||
return Err(anyhow::anyhow!( | ||
"Requesting the 'latest' Python version is not yet supported" | ||
) | ||
.into()) | ||
} | ||
// From the definition of `is_python`, this can only be a bare `python` | ||
Target::From(_, from) => { | ||
debug_assert_eq!(*from, "python"); | ||
None | ||
} | ||
}; | ||
|
||
if let Some(target_request) = &target_request { | ||
if let Some(python) = python { | ||
return Err(anyhow::anyhow!( | ||
"Received multiple Python version requests: `{}` and `{}`", | ||
python.to_string().cyan(), | ||
target_request.to_canonical_string().cyan(), | ||
) | ||
.into()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is like, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. We could check for compatibility or merge them or whatever because |
||
} | ||
} | ||
|
||
target_request.or_else(|| python.map(PythonRequest::parse)) | ||
} else { | ||
python.map(PythonRequest::parse) | ||
}; | ||
|
||
// Discover an interpreter. | ||
let interpreter = PythonInstallation::find_or_download( | ||
|
@@ -448,66 +513,82 @@ async fn get_or_create_environment( | |
// Initialize any shared state. | ||
let state = PlatformState::default(); | ||
|
||
// Resolve the `--from` requirement. | ||
let from = match target { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is mostly just indented in the if else |
||
// Ex) `ruff` | ||
Target::Unspecified(name) => Requirement { | ||
name: PackageName::from_str(name)?, | ||
extras: vec![], | ||
groups: vec![], | ||
marker: MarkerTree::default(), | ||
source: RequirementSource::Registry { | ||
specifier: VersionSpecifiers::empty(), | ||
index: None, | ||
conflict: None, | ||
let from = if target.is_python() { | ||
ToolRequirement::Python | ||
} else { | ||
ToolRequirement::Package(match target { | ||
// Ex) `ruff` | ||
Target::Unspecified(name) => Requirement { | ||
name: PackageName::from_str(name)?, | ||
extras: vec![], | ||
groups: vec![], | ||
marker: MarkerTree::default(), | ||
source: RequirementSource::Registry { | ||
specifier: VersionSpecifiers::empty(), | ||
index: None, | ||
conflict: None, | ||
}, | ||
origin: None, | ||
}, | ||
origin: None, | ||
}, | ||
// Ex) `ruff@0.6.0` | ||
Target::Version(name, version) | Target::FromVersion(_, name, version) => Requirement { | ||
name: PackageName::from_str(name)?, | ||
extras: vec![], | ||
groups: vec![], | ||
marker: MarkerTree::default(), | ||
source: RequirementSource::Registry { | ||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version( | ||
version.clone(), | ||
)), | ||
index: None, | ||
conflict: None, | ||
// Ex) `ruff@0.6.0` | ||
Target::Version(name, version) | Target::FromVersion(_, name, version) => Requirement { | ||
name: PackageName::from_str(name)?, | ||
extras: vec![], | ||
groups: vec![], | ||
marker: MarkerTree::default(), | ||
source: RequirementSource::Registry { | ||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version( | ||
version.clone(), | ||
)), | ||
index: None, | ||
conflict: None, | ||
}, | ||
origin: None, | ||
}, | ||
origin: None, | ||
}, | ||
// Ex) `ruff@latest` | ||
Target::Latest(name) | Target::FromLatest(_, name) => Requirement { | ||
name: PackageName::from_str(name)?, | ||
extras: vec![], | ||
groups: vec![], | ||
marker: MarkerTree::default(), | ||
source: RequirementSource::Registry { | ||
specifier: VersionSpecifiers::empty(), | ||
index: None, | ||
conflict: None, | ||
// Ex) `ruff@latest` | ||
Target::Latest(name) | Target::FromLatest(_, name) => Requirement { | ||
name: PackageName::from_str(name)?, | ||
extras: vec![], | ||
groups: vec![], | ||
marker: MarkerTree::default(), | ||
source: RequirementSource::Registry { | ||
specifier: VersionSpecifiers::empty(), | ||
index: None, | ||
conflict: None, | ||
}, | ||
origin: None, | ||
}, | ||
origin: None, | ||
}, | ||
// Ex) `ruff>=0.6.0` | ||
Target::From(_, from) => resolve_names( | ||
vec![RequirementsSpecification::parse_package(from)?], | ||
&interpreter, | ||
settings, | ||
&state, | ||
connectivity, | ||
concurrency, | ||
native_tls, | ||
allow_insecure_host, | ||
cache, | ||
printer, | ||
preview, | ||
) | ||
.await? | ||
.pop() | ||
.unwrap(), | ||
// Ex) `ruff>=0.6.0` | ||
Target::From(_, from) => { | ||
let spec = RequirementsSpecification::parse_package(from)?; | ||
if let UnresolvedRequirement::Named(requirement) = &spec.requirement { | ||
if requirement.name.as_str() == "python" { | ||
return Err(anyhow::anyhow!( | ||
"Using `{}` is not supported. Use `{}` instead.", | ||
"--from python<specifier>".cyan(), | ||
"python@<version>".cyan(), | ||
) | ||
.into()); | ||
} | ||
} | ||
resolve_names( | ||
vec![spec], | ||
&interpreter, | ||
settings, | ||
&state, | ||
connectivity, | ||
concurrency, | ||
native_tls, | ||
allow_insecure_host, | ||
cache, | ||
printer, | ||
preview, | ||
) | ||
.await? | ||
.pop() | ||
.unwrap() | ||
} | ||
}) | ||
}; | ||
|
||
// Read the `--with` requirements. | ||
|
@@ -522,7 +603,10 @@ async fn get_or_create_environment( | |
// Resolve the `--from` and `--with` requirements. | ||
let requirements = { | ||
let mut requirements = Vec::with_capacity(1 + with.len()); | ||
requirements.push(from.clone()); | ||
match &from { | ||
ToolRequirement::Python => {} | ||
ToolRequirement::Package(requirement) => requirements.push(requirement.clone()), | ||
} | ||
requirements.extend( | ||
resolve_names( | ||
spec.requirements.clone(), | ||
|
@@ -547,35 +631,36 @@ async fn get_or_create_environment( | |
let installed_tools = InstalledTools::from_settings()?.init()?; | ||
let _lock = installed_tools.lock().await?; | ||
|
||
let existing_environment = | ||
installed_tools | ||
.get_environment(&from.name, cache)? | ||
if let ToolRequirement::Package(requirement) = &from { | ||
let existing_environment = installed_tools | ||
.get_environment(&requirement.name, cache)? | ||
.filter(|environment| { | ||
python_request.as_ref().map_or(true, |python_request| { | ||
python_request.satisfied(environment.interpreter(), cache) | ||
}) | ||
}); | ||
if let Some(environment) = existing_environment { | ||
// Check if the installed packages meet the requirements. | ||
let site_packages = SitePackages::from_environment(&environment)?; | ||
if let Some(environment) = existing_environment { | ||
// Check if the installed packages meet the requirements. | ||
let site_packages = SitePackages::from_environment(&environment)?; | ||
|
||
let requirements = requirements | ||
.iter() | ||
.cloned() | ||
.map(UnresolvedRequirementSpecification::from) | ||
.collect::<Vec<_>>(); | ||
let constraints = []; | ||
|
||
if matches!( | ||
site_packages.satisfies( | ||
&requirements, | ||
&constraints, | ||
&interpreter.resolver_marker_environment() | ||
), | ||
Ok(SatisfiesResult::Fresh { .. }) | ||
) { | ||
debug!("Using existing tool `{}`", from.name); | ||
return Ok((from, environment)); | ||
let requirements = requirements | ||
.iter() | ||
.cloned() | ||
.map(UnresolvedRequirementSpecification::from) | ||
.collect::<Vec<_>>(); | ||
let constraints = []; | ||
|
||
if matches!( | ||
site_packages.satisfies( | ||
&requirements, | ||
&constraints, | ||
&interpreter.resolver_marker_environment() | ||
), | ||
Ok(SatisfiesResult::Fresh { .. }) | ||
) { | ||
debug!("Using existing tool `{}`", requirement.name); | ||
return Ok((from, environment)); | ||
} | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to do anything to ensure that
python
resolves to the "correct" Python? Is there any risk that the user does, like,uvx --python 3.7 python
and we end up picking a Python other than the discovered interpreter?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not consider that; I will add a guard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opted not to do this as it complicates the implementation and I'm pretty sure there can't be a problem in-practice. We do the same thing for actual tool executables.