mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-12 08:51:16 +00:00
Add support for uvx python (#11076)
Supersedes https://github.com/astral-sh/uv/pull/7491 Closes https://github.com/astral-sh/uv/issues/7430 Thanks @mikeleppane for starting this implementation. I took a bit of a different approach and it was easier to start over fresh, but I used some of the test cases there.
This commit is contained in:
parent
d106ab1a9a
commit
220821bc39
7 changed files with 426 additions and 89 deletions
|
|
@ -3712,6 +3712,9 @@ pub enum ToolCommand {
|
||||||
/// e.g., `uv tool run ruff@0.3.0`. If more complex version specification is desired or if the
|
/// e.g., `uv tool run ruff@0.3.0`. If more complex version specification is desired or if the
|
||||||
/// command is provided by a different package, use `--from`.
|
/// command is provided by a different package, use `--from`.
|
||||||
///
|
///
|
||||||
|
/// `uvx` can be used to invoke Python, e.g., with `uvx python` or `uvx python@<version>`. A
|
||||||
|
/// Python interpreter will be started in an isolated virtual environment.
|
||||||
|
///
|
||||||
/// If the tool was previously installed, i.e., via `uv tool install`, the installed version
|
/// If the tool was previously installed, i.e., via `uv tool install`, the installed version
|
||||||
/// will be used unless a version is requested or the `--isolated` flag is used.
|
/// will be used unless a version is requested or the `--isolated` flag is used.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,14 @@ pub(crate) async fn install(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if from.name.as_str().eq_ignore_ascii_case("python") {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Cannot install Python with `{}`. Did you mean to use `{}`?",
|
||||||
|
"uv tool install".cyan(),
|
||||||
|
"uv python install".cyan(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// If the user passed, e.g., `ruff@latest`, we need to mark it as upgradable.
|
// If the user passed, e.g., `ruff@latest`, we need to mark it as upgradable.
|
||||||
let settings = if target.is_latest() {
|
let settings = if target.is_latest() {
|
||||||
ResolverInstallerSettings {
|
ResolverInstallerSettings {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,19 @@ impl<'a> Target<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether the target package is Python.
|
||||||
|
pub(crate) fn is_python(&self) -> bool {
|
||||||
|
let name = match self {
|
||||||
|
Self::Unspecified(name) => name,
|
||||||
|
Self::Version(name, _) => name,
|
||||||
|
Self::Latest(name) => name,
|
||||||
|
Self::FromVersion(_, name, _) => name,
|
||||||
|
Self::FromLatest(_, name) => name,
|
||||||
|
Self::From(_, name) => name,
|
||||||
|
};
|
||||||
|
name.eq_ignore_ascii_case("python") || cfg!(windows) && name.eq_ignore_ascii_case("pythonw")
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if the target is `latest`.
|
/// Returns `true` if the target is `latest`.
|
||||||
fn is_latest(&self) -> bool {
|
fn is_latest(&self) -> bool {
|
||||||
matches!(self, Self::Latest(_) | Self::FromLatest(_, _))
|
matches!(self, Self::Latest(_) | Self::FromLatest(_, _))
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,14 @@ use uv_cache_info::Timestamp;
|
||||||
use uv_cli::ExternalCommand;
|
use uv_cli::ExternalCommand;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
|
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
|
||||||
|
use uv_distribution_types::UnresolvedRequirement;
|
||||||
use uv_distribution_types::{Name, UnresolvedRequirementSpecification};
|
use uv_distribution_types::{Name, UnresolvedRequirementSpecification};
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||||
use uv_pep508::MarkerTree;
|
use uv_pep508::MarkerTree;
|
||||||
use uv_pypi_types::{Requirement, RequirementSource};
|
use uv_pypi_types::{Requirement, RequirementSource};
|
||||||
|
use uv_python::VersionRequest;
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
|
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||||
PythonPreference, PythonRequest,
|
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.
|
// 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.
|
// If the command is found in other packages, we warn the user about the correct package to use.
|
||||||
warn_executable_not_provided_by_package(
|
match &from {
|
||||||
executable,
|
ToolRequirement::Python => {}
|
||||||
&from.name,
|
ToolRequirement::Package(from) => {
|
||||||
&site_packages,
|
warn_executable_not_provided_by_package(
|
||||||
invocation_source,
|
executable,
|
||||||
);
|
&from.name,
|
||||||
|
&site_packages,
|
||||||
|
invocation_source,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let handle = match process.spawn() {
|
let handle = match process.spawn() {
|
||||||
Ok(handle) => Ok(handle),
|
Ok(handle) => Ok(handle),
|
||||||
|
|
@ -216,11 +223,15 @@ pub(crate) async fn run(
|
||||||
/// Returns an exit status if the caller should exit after hinting.
|
/// Returns an exit status if the caller should exit after hinting.
|
||||||
fn hint_on_not_found(
|
fn hint_on_not_found(
|
||||||
executable: &str,
|
executable: &str,
|
||||||
from: &Requirement,
|
from: &ToolRequirement,
|
||||||
site_packages: &SitePackages,
|
site_packages: &SitePackages,
|
||||||
invocation_source: ToolRunCommand,
|
invocation_source: ToolRunCommand,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> anyhow::Result<Option<ExitStatus>> {
|
) -> anyhow::Result<Option<ExitStatus>> {
|
||||||
|
let from = match from {
|
||||||
|
ToolRequirement::Python => return Ok(None),
|
||||||
|
ToolRequirement::Package(from) => from,
|
||||||
|
};
|
||||||
match get_entrypoints(&from.name, site_packages) {
|
match get_entrypoints(&from.name, site_packages) {
|
||||||
Ok(entrypoints) => {
|
Ok(entrypoints) => {
|
||||||
writeln!(
|
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.
|
/// 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
|
/// If the target tool is already installed in a compatible environment, returns that
|
||||||
|
|
@ -420,7 +448,7 @@ async fn get_or_create_environment(
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> Result<(Requirement, PythonEnvironment), ProjectError> {
|
) -> Result<(ToolRequirement, PythonEnvironment), ProjectError> {
|
||||||
let client_builder = BaseClientBuilder::new()
|
let client_builder = BaseClientBuilder::new()
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
|
|
@ -428,7 +456,44 @@ async fn get_or_create_environment(
|
||||||
|
|
||||||
let reporter = PythonDownloadReporter::single(printer);
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target_request.or_else(|| python.map(PythonRequest::parse))
|
||||||
|
} else {
|
||||||
|
python.map(PythonRequest::parse)
|
||||||
|
};
|
||||||
|
|
||||||
// Discover an interpreter.
|
// Discover an interpreter.
|
||||||
let interpreter = PythonInstallation::find_or_download(
|
let interpreter = PythonInstallation::find_or_download(
|
||||||
|
|
@ -448,66 +513,82 @@ async fn get_or_create_environment(
|
||||||
// Initialize any shared state.
|
// Initialize any shared state.
|
||||||
let state = PlatformState::default();
|
let state = PlatformState::default();
|
||||||
|
|
||||||
// Resolve the `--from` requirement.
|
let from = if target.is_python() {
|
||||||
let from = match target {
|
ToolRequirement::Python
|
||||||
// Ex) `ruff`
|
} else {
|
||||||
Target::Unspecified(name) => Requirement {
|
ToolRequirement::Package(match target {
|
||||||
name: PackageName::from_str(name)?,
|
// Ex) `ruff`
|
||||||
extras: vec![],
|
Target::Unspecified(name) => Requirement {
|
||||||
groups: vec![],
|
name: PackageName::from_str(name)?,
|
||||||
marker: MarkerTree::default(),
|
extras: vec![],
|
||||||
source: RequirementSource::Registry {
|
groups: vec![],
|
||||||
specifier: VersionSpecifiers::empty(),
|
marker: MarkerTree::default(),
|
||||||
index: None,
|
source: RequirementSource::Registry {
|
||||||
conflict: None,
|
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 {
|
||||||
// Ex) `ruff@0.6.0`
|
name: PackageName::from_str(name)?,
|
||||||
Target::Version(name, version) | Target::FromVersion(_, name, version) => Requirement {
|
extras: vec![],
|
||||||
name: PackageName::from_str(name)?,
|
groups: vec![],
|
||||||
extras: vec![],
|
marker: MarkerTree::default(),
|
||||||
groups: vec![],
|
source: RequirementSource::Registry {
|
||||||
marker: MarkerTree::default(),
|
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
|
||||||
source: RequirementSource::Registry {
|
version.clone(),
|
||||||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
|
)),
|
||||||
version.clone(),
|
index: None,
|
||||||
)),
|
conflict: None,
|
||||||
index: None,
|
},
|
||||||
conflict: None,
|
origin: None,
|
||||||
},
|
},
|
||||||
origin: None,
|
// Ex) `ruff@latest`
|
||||||
},
|
Target::Latest(name) | Target::FromLatest(_, name) => Requirement {
|
||||||
// Ex) `ruff@latest`
|
name: PackageName::from_str(name)?,
|
||||||
Target::Latest(name) | Target::FromLatest(_, name) => Requirement {
|
extras: vec![],
|
||||||
name: PackageName::from_str(name)?,
|
groups: vec![],
|
||||||
extras: vec![],
|
marker: MarkerTree::default(),
|
||||||
groups: vec![],
|
source: RequirementSource::Registry {
|
||||||
marker: MarkerTree::default(),
|
specifier: VersionSpecifiers::empty(),
|
||||||
source: RequirementSource::Registry {
|
index: None,
|
||||||
specifier: VersionSpecifiers::empty(),
|
conflict: None,
|
||||||
index: None,
|
},
|
||||||
conflict: None,
|
origin: None,
|
||||||
},
|
},
|
||||||
origin: None,
|
// Ex) `ruff>=0.6.0`
|
||||||
},
|
Target::From(_, from) => {
|
||||||
// Ex) `ruff>=0.6.0`
|
let spec = RequirementsSpecification::parse_package(from)?;
|
||||||
Target::From(_, from) => resolve_names(
|
if let UnresolvedRequirement::Named(requirement) = &spec.requirement {
|
||||||
vec![RequirementsSpecification::parse_package(from)?],
|
if requirement.name.as_str() == "python" {
|
||||||
&interpreter,
|
return Err(anyhow::anyhow!(
|
||||||
settings,
|
"Using `{}` is not supported. Use `{}` instead.",
|
||||||
&state,
|
"--from python<specifier>".cyan(),
|
||||||
connectivity,
|
"python@<version>".cyan(),
|
||||||
concurrency,
|
)
|
||||||
native_tls,
|
.into());
|
||||||
allow_insecure_host,
|
}
|
||||||
cache,
|
}
|
||||||
printer,
|
resolve_names(
|
||||||
preview,
|
vec![spec],
|
||||||
)
|
&interpreter,
|
||||||
.await?
|
settings,
|
||||||
.pop()
|
&state,
|
||||||
.unwrap(),
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
allow_insecure_host,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
preview,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.pop()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read the `--with` requirements.
|
// Read the `--with` requirements.
|
||||||
|
|
@ -522,7 +603,10 @@ async fn get_or_create_environment(
|
||||||
// Resolve the `--from` and `--with` requirements.
|
// Resolve the `--from` and `--with` requirements.
|
||||||
let requirements = {
|
let requirements = {
|
||||||
let mut requirements = Vec::with_capacity(1 + with.len());
|
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(
|
requirements.extend(
|
||||||
resolve_names(
|
resolve_names(
|
||||||
spec.requirements.clone(),
|
spec.requirements.clone(),
|
||||||
|
|
@ -547,35 +631,36 @@ async fn get_or_create_environment(
|
||||||
let installed_tools = InstalledTools::from_settings()?.init()?;
|
let installed_tools = InstalledTools::from_settings()?.init()?;
|
||||||
let _lock = installed_tools.lock().await?;
|
let _lock = installed_tools.lock().await?;
|
||||||
|
|
||||||
let existing_environment =
|
if let ToolRequirement::Package(requirement) = &from {
|
||||||
installed_tools
|
let existing_environment = installed_tools
|
||||||
.get_environment(&from.name, cache)?
|
.get_environment(&requirement.name, cache)?
|
||||||
.filter(|environment| {
|
.filter(|environment| {
|
||||||
python_request.as_ref().map_or(true, |python_request| {
|
python_request.as_ref().map_or(true, |python_request| {
|
||||||
python_request.satisfied(environment.interpreter(), cache)
|
python_request.satisfied(environment.interpreter(), cache)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if let Some(environment) = existing_environment {
|
if let Some(environment) = existing_environment {
|
||||||
// Check if the installed packages meet the requirements.
|
// Check if the installed packages meet the requirements.
|
||||||
let site_packages = SitePackages::from_environment(&environment)?;
|
let site_packages = SitePackages::from_environment(&environment)?;
|
||||||
|
|
||||||
let requirements = requirements
|
let requirements = requirements
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(UnresolvedRequirementSpecification::from)
|
.map(UnresolvedRequirementSpecification::from)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let constraints = [];
|
let constraints = [];
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
site_packages.satisfies(
|
site_packages.satisfies(
|
||||||
&requirements,
|
&requirements,
|
||||||
&constraints,
|
&constraints,
|
||||||
&interpreter.resolver_marker_environment()
|
&interpreter.resolver_marker_environment()
|
||||||
),
|
),
|
||||||
Ok(SatisfiesResult::Fresh { .. })
|
Ok(SatisfiesResult::Fresh { .. })
|
||||||
) {
|
) {
|
||||||
debug!("Using existing tool `{}`", from.name);
|
debug!("Using existing tool `{}`", requirement.name);
|
||||||
return Ok((from, environment));
|
return Ok((from, environment));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3313,3 +3313,41 @@ fn tool_install_overrides() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `uv tool install python` is not allowed
|
||||||
|
#[test]
|
||||||
|
fn tool_install_python() {
|
||||||
|
let context = TestContext::new("3.12")
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
// Install `python`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("python")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||||
|
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Cannot install Python with `uv tool install`. Did you mean to use `uv python install`?
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Install `python@<version>`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("python@3.12")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||||
|
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Cannot install Python with `uv tool install`. Did you mean to use `uv python install`?
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1397,3 +1397,191 @@ fn tool_run_latest() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_python() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("print('Hello, world!')"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Hello, world!
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_python_at_version() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_python_sources();
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python@3.12")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python@3.11")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.11.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request a version via `-p`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("-p")
|
||||||
|
.arg("3.11")
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.11.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request a version in the tool and `-p`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("-p")
|
||||||
|
.arg("3.12")
|
||||||
|
.arg("python@3.11")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Received multiple Python version requests: `3.12` and `3.11`
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request a version that does not exist
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python@3.12.99"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No interpreter found for Python 3.12.[X] in [PYTHON SOURCES]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request an invalid version
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python@3.300"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Invalid version request: 3.300
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request `@latest` (not yet supported)
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python@latest")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Requesting the 'latest' Python version is not yet supported
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_python_from() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_python_sources();
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("--from")
|
||||||
|
.arg("python")
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("--from")
|
||||||
|
.arg("python@3.11")
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.11.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("--from")
|
||||||
|
.arg("python>=3.12")
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Using `--from python<specifier>` is not supported. Use `python@<version>` instead.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3037,6 +3037,8 @@ By default, the package to install is assumed to match the command name.
|
||||||
|
|
||||||
The name of the command can include an exact version in the format `<package>@<version>`, e.g., `uv tool run ruff@0.3.0`. If more complex version specification is desired or if the command is provided by a different package, use `--from`.
|
The name of the command can include an exact version in the format `<package>@<version>`, e.g., `uv tool run ruff@0.3.0`. If more complex version specification is desired or if the command is provided by a different package, use `--from`.
|
||||||
|
|
||||||
|
`uvx` can be used to invoke Python, e.g., with `uvx python` or `uvx python@<version>`. A Python interpreter will be started in an isolated virtual environment.
|
||||||
|
|
||||||
If the tool was previously installed, i.e., via `uv tool install`, the installed version will be used unless a version is requested or the `--isolated` flag is used.
|
If the tool was previously installed, i.e., via `uv tool install`, the installed version will be used unless a version is requested or the `--isolated` flag is used.
|
||||||
|
|
||||||
`uvx` is provided as a convenient alias for `uv tool run`, their behavior is identical.
|
`uvx` is provided as a convenient alias for `uv tool run`, their behavior is identical.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue