mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +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
|
||||
/// 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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
let settings = if target.is_latest() {
|
||||
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`.
|
||||
fn is_latest(&self) -> bool {
|
||||
matches!(self, Self::Latest(_) | Self::FromLatest(_, _))
|
||||
|
|
|
|||
|
|
@ -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,7 +448,7 @@ 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)
|
||||
|
|
@ -428,7 +456,44 @@ async fn get_or_create_environment(
|
|||
|
||||
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.
|
||||
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 {
|
||||
// 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 = [];
|
||||
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));
|
||||
if matches!(
|
||||
site_packages.satisfies(
|
||||
&requirements,
|
||||
&constraints,
|
||||
&interpreter.resolver_marker_environment()
|
||||
),
|
||||
Ok(SatisfiesResult::Fresh { .. })
|
||||
) {
|
||||
debug!("Using existing tool `{}`", requirement.name);
|
||||
return Ok((from, environment));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3313,3 +3313,41 @@ fn tool_install_overrides() -> Result<()> {
|
|||
|
||||
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 -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[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`.
|
||||
|
||||
`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.
|
||||
|
||||
`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