mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
Add support for upgrading Python in tool environments (#7605)
This PR adds support for upgrading the build environment of tools with the addition of a ```--python``` argument to ```uv upgrade```, as specified in #7471. Some things to note: - I added support for individual packages — I didn't think there was a good reason for ```--python``` to only apply to all packages - Upgrading with ```--python``` also upgrades the package itself — I think this is fair as if a user wants to _strictly_ switch the version of Python being used to build a tool's environment they can use ```uv install```. This behavior can of course be modified if others don't agree! Closes https://github.com/astral-sh/uv/issues/6297. Closes https://github.com/astral-sh/uv/issues/7471.
This commit is contained in:
parent
f5601e2610
commit
106633a5e5
10 changed files with 394 additions and 53 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4490,7 +4490,6 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"same-file",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"similar",
|
"similar",
|
||||||
|
|
|
@ -3396,6 +3396,20 @@ pub struct ToolUpgradeArgs {
|
||||||
#[arg(long, conflicts_with("name"))]
|
#[arg(long, conflicts_with("name"))]
|
||||||
pub all: bool,
|
pub all: bool,
|
||||||
|
|
||||||
|
/// Upgrade a tool, and specify it to use the given Python interpreter
|
||||||
|
/// to build its environment. Use with `--all` to apply to all tools.
|
||||||
|
///
|
||||||
|
/// See `uv help python` for details on Python discovery and supported
|
||||||
|
/// request formats.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
env = "UV_PYTHON",
|
||||||
|
verbatim_doc_comment,
|
||||||
|
help_heading = "Python options"
|
||||||
|
)]
|
||||||
|
pub python: Option<String>,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub installer: ResolverInstallerArgs,
|
pub installer: ResolverInstallerArgs,
|
||||||
|
|
||||||
|
|
|
@ -300,4 +300,26 @@ impl PythonEnvironment {
|
||||||
pub fn into_interpreter(self) -> Interpreter {
|
pub fn into_interpreter(self) -> Interpreter {
|
||||||
Arc::unwrap_or_clone(self.0).interpreter
|
Arc::unwrap_or_clone(self.0).interpreter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the [`PythonEnvironment`] uses the same underlying [`Interpreter`].
|
||||||
|
pub fn uses(&self, interpreter: &Interpreter) -> bool {
|
||||||
|
// TODO(zanieb): Consider using `sysconfig.get_path("stdlib")` instead, which
|
||||||
|
// should be generally robust.
|
||||||
|
if cfg!(windows) {
|
||||||
|
// On Windows, we can't canonicalize an interpreter based on its executable path
|
||||||
|
// because the executables are separate shim files (not links). Instead, we
|
||||||
|
// compare the `sys.base_prefix`.
|
||||||
|
let old_base_prefix = self.interpreter().sys_base_prefix();
|
||||||
|
let selected_base_prefix = interpreter.sys_base_prefix();
|
||||||
|
old_base_prefix == selected_base_prefix
|
||||||
|
} else {
|
||||||
|
// On Unix, we can see if the canonicalized executable is the same file.
|
||||||
|
self.interpreter().sys_executable() == interpreter.sys_executable()
|
||||||
|
|| same_file::is_same_file(
|
||||||
|
self.interpreter().sys_executable(),
|
||||||
|
interpreter.sys_executable(),
|
||||||
|
)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,6 @@ owo-colors = { workspace = true }
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
same-file = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
|
@ -277,23 +277,7 @@ pub(crate) async fn install(
|
||||||
installed_tools
|
installed_tools
|
||||||
.get_environment(&from.name, &cache)?
|
.get_environment(&from.name, &cache)?
|
||||||
.filter(|environment| {
|
.filter(|environment| {
|
||||||
// TODO(zanieb): Consider using `sysconfig.get_path("stdlib")` instead, which
|
if environment.uses(&interpreter) {
|
||||||
// should be generally robust.
|
|
||||||
// TODO(zanieb): Move this into a utility on `Interpreter` since it's non-trivial.
|
|
||||||
let same_interpreter = if cfg!(windows) {
|
|
||||||
// On Windows, we can't canonicalize an interpreter based on its executable path
|
|
||||||
// because the executables are separate shim files (not links). Instead, we
|
|
||||||
// compare the `sys.base_prefix`.
|
|
||||||
let old_base_prefix = environment.interpreter().sys_base_prefix();
|
|
||||||
let selected_base_prefix = interpreter.sys_base_prefix();
|
|
||||||
old_base_prefix == selected_base_prefix
|
|
||||||
} else {
|
|
||||||
// On Unix, we can see if the canonicalized executable is the same file.
|
|
||||||
environment.interpreter().sys_executable() == interpreter.sys_executable()
|
|
||||||
|| same_file::is_same_file(environment.interpreter().sys_executable(), interpreter.sys_executable()).unwrap_or(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
if same_interpreter {
|
|
||||||
trace!(
|
trace!(
|
||||||
"Existing interpreter matches the requested interpreter for `{}`: {}",
|
"Existing interpreter matches the requested interpreter for `{}`: {}",
|
||||||
from.name,
|
from.name,
|
||||||
|
|
|
@ -5,16 +5,24 @@ use owo_colors::OwoColorize;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::Connectivity;
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::Concurrency;
|
use uv_configuration::Concurrency;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
use uv_python::{
|
||||||
|
EnvironmentPreference, Interpreter, PythonDownloads, PythonInstallation, PythonPreference,
|
||||||
|
PythonRequest,
|
||||||
|
};
|
||||||
use uv_requirements::RequirementsSpecification;
|
use uv_requirements::RequirementsSpecification;
|
||||||
use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions};
|
use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions};
|
||||||
use uv_tool::InstalledTools;
|
use uv_tool::InstalledTools;
|
||||||
|
|
||||||
use crate::commands::pip::loggers::{SummaryResolveLogger, UpgradeInstallLogger};
|
use crate::commands::pip::loggers::{
|
||||||
use crate::commands::pip::operations::Changelog;
|
DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger,
|
||||||
use crate::commands::project::{update_environment, EnvironmentUpdate};
|
};
|
||||||
|
use crate::commands::project::{
|
||||||
|
resolve_environment, sync_environment, update_environment, EnvironmentUpdate,
|
||||||
|
};
|
||||||
|
use crate::commands::reporters::PythonDownloadReporter;
|
||||||
use crate::commands::tool::common::remove_entrypoints;
|
use crate::commands::tool::common::remove_entrypoints;
|
||||||
use crate::commands::{tool::common::install_executables, ExitStatus, SharedState};
|
use crate::commands::{tool::common::install_executables, ExitStatus, SharedState};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
@ -23,9 +31,12 @@ use crate::settings::ResolverInstallerSettings;
|
||||||
/// Upgrade a tool.
|
/// Upgrade a tool.
|
||||||
pub(crate) async fn upgrade(
|
pub(crate) async fn upgrade(
|
||||||
name: Vec<PackageName>,
|
name: Vec<PackageName>,
|
||||||
|
python: Option<String>,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
args: ResolverInstallerOptions,
|
args: ResolverInstallerOptions,
|
||||||
filesystem: ResolverInstallerOptions,
|
filesystem: ResolverInstallerOptions,
|
||||||
|
python_preference: PythonPreference,
|
||||||
|
python_downloads: PythonDownloads,
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
|
@ -34,6 +45,7 @@ pub(crate) async fn upgrade(
|
||||||
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?;
|
||||||
|
|
||||||
|
// Collect the tools to upgrade.
|
||||||
let names: BTreeSet<PackageName> = {
|
let names: BTreeSet<PackageName> = {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
installed_tools
|
installed_tools
|
||||||
|
@ -52,16 +64,45 @@ pub(crate) async fn upgrade(
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reporter = PythonDownloadReporter::single(printer);
|
||||||
|
let client_builder = BaseClientBuilder::new()
|
||||||
|
.connectivity(connectivity)
|
||||||
|
.native_tls(native_tls);
|
||||||
|
|
||||||
|
let python_request = python.as_deref().map(PythonRequest::parse);
|
||||||
|
|
||||||
|
let interpreter = if python_request.is_some() {
|
||||||
|
Some(
|
||||||
|
PythonInstallation::find_or_download(
|
||||||
|
python_request.as_ref(),
|
||||||
|
EnvironmentPreference::OnlySystem,
|
||||||
|
python_preference,
|
||||||
|
python_downloads,
|
||||||
|
&client_builder,
|
||||||
|
cache,
|
||||||
|
Some(&reporter),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_interpreter(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Determine whether we applied any upgrades.
|
// Determine whether we applied any upgrades.
|
||||||
let mut did_upgrade = false;
|
let mut did_upgrade_tool = vec![];
|
||||||
|
|
||||||
|
// Determine whether we applied any upgrades.
|
||||||
|
let mut did_upgrade_environment = vec![];
|
||||||
|
|
||||||
// Determine whether any tool upgrade failed.
|
// Determine whether any tool upgrade failed.
|
||||||
let mut failed_upgrade = false;
|
let mut failed_upgrade = false;
|
||||||
|
|
||||||
for name in &names {
|
for name in &names {
|
||||||
debug!("Upgrading tool: `{name}`");
|
debug!("Upgrading tool: `{name}`");
|
||||||
let changelog = upgrade_tool(
|
let result = upgrade_tool(
|
||||||
name,
|
name,
|
||||||
|
interpreter.as_ref(),
|
||||||
printer,
|
printer,
|
||||||
&installed_tools,
|
&installed_tools,
|
||||||
&args,
|
&args,
|
||||||
|
@ -73,9 +114,15 @@ pub(crate) async fn upgrade(
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match changelog {
|
match result {
|
||||||
Ok(changelog) => {
|
Ok(UpgradeOutcome::UpgradeEnvironment) => {
|
||||||
did_upgrade |= !changelog.is_empty();
|
did_upgrade_environment.push(name);
|
||||||
|
}
|
||||||
|
Ok(UpgradeOutcome::UpgradeDependencies | UpgradeOutcome::UpgradeTool) => {
|
||||||
|
did_upgrade_tool.push(name);
|
||||||
|
}
|
||||||
|
Ok(UpgradeOutcome::NoOp) => {
|
||||||
|
debug!("Upgrading `{name}` was a no-op");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// If we have a single tool, return the error directly.
|
// If we have a single tool, return the error directly.
|
||||||
|
@ -97,15 +144,43 @@ pub(crate) async fn upgrade(
|
||||||
return Ok(ExitStatus::Failure);
|
return Ok(ExitStatus::Failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !did_upgrade {
|
if did_upgrade_tool.is_empty() && did_upgrade_environment.is_empty() {
|
||||||
writeln!(printer.stderr(), "Nothing to upgrade")?;
|
writeln!(printer.stderr(), "Nothing to upgrade")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(python_request) = python_request {
|
||||||
|
let tools = did_upgrade_environment
|
||||||
|
.iter()
|
||||||
|
.map(|name| format!("`{}`", name.cyan()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let s = if tools.len() > 1 { "s" } else { "" };
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Upgraded tool environment{s} for {} to {}",
|
||||||
|
conjunction(tools),
|
||||||
|
python_request.cyan(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum UpgradeOutcome {
|
||||||
|
/// The tool itself was upgraded.
|
||||||
|
UpgradeTool,
|
||||||
|
/// The tool's dependencies were upgraded, but the tool itself was unchanged.
|
||||||
|
UpgradeDependencies,
|
||||||
|
/// The tool's environment was upgraded.
|
||||||
|
UpgradeEnvironment,
|
||||||
|
/// The tool was already up-to-date.
|
||||||
|
NoOp,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upgrade a specific tool.
|
||||||
async fn upgrade_tool(
|
async fn upgrade_tool(
|
||||||
name: &PackageName,
|
name: &PackageName,
|
||||||
|
interpreter: Option<&Interpreter>,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
installed_tools: &InstalledTools,
|
installed_tools: &InstalledTools,
|
||||||
args: &ResolverInstallerOptions,
|
args: &ResolverInstallerOptions,
|
||||||
|
@ -114,7 +189,7 @@ async fn upgrade_tool(
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
) -> Result<Changelog> {
|
) -> Result<UpgradeOutcome> {
|
||||||
// Ensure the tool is installed.
|
// Ensure the tool is installed.
|
||||||
let existing_tool_receipt = match installed_tools.get_tool_receipt(name) {
|
let existing_tool_receipt = match installed_tools.get_tool_receipt(name) {
|
||||||
Ok(Some(receipt)) => receipt,
|
Ok(Some(receipt)) => receipt,
|
||||||
|
@ -136,7 +211,7 @@ async fn upgrade_tool(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let existing_environment = match installed_tools.get_environment(name, cache) {
|
let environment = match installed_tools.get_environment(name, cache) {
|
||||||
Ok(Some(environment)) => environment,
|
Ok(Some(environment)) => environment,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
let install_command = format!("uv tool install {name}");
|
let install_command = format!("uv tool install {name}");
|
||||||
|
@ -170,32 +245,85 @@ async fn upgrade_tool(
|
||||||
// Initialize any shared state.
|
// Initialize any shared state.
|
||||||
let state = SharedState::default();
|
let state = SharedState::default();
|
||||||
|
|
||||||
// TODO(zanieb): Build the environment in the cache directory then copy into the tool
|
// Check if we need to create a new environment — if so, resolve it first, then
|
||||||
// directory.
|
// install the requested tool
|
||||||
let EnvironmentUpdate {
|
let (environment, outcome) = if let Some(interpreter) =
|
||||||
environment,
|
interpreter.filter(|interpreter| !environment.uses(interpreter))
|
||||||
changelog,
|
{
|
||||||
} = update_environment(
|
// If we're using a new interpreter, re-create the environment for each tool.
|
||||||
existing_environment,
|
let resolution = resolve_environment(
|
||||||
spec,
|
RequirementsSpecification::from_requirements(requirements.to_vec()).into(),
|
||||||
&settings,
|
interpreter,
|
||||||
&state,
|
settings.as_ref().into(),
|
||||||
Box::new(SummaryResolveLogger),
|
&state,
|
||||||
Box::new(UpgradeInstallLogger::new(name.clone())),
|
Box::new(SummaryResolveLogger),
|
||||||
connectivity,
|
connectivity,
|
||||||
concurrency,
|
concurrency,
|
||||||
native_tls,
|
native_tls,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// If we modified the target tool, reinstall the entrypoints.
|
let environment = installed_tools.create_environment(name, interpreter.clone())?;
|
||||||
if changelog.includes(name) {
|
|
||||||
|
let environment = sync_environment(
|
||||||
|
environment,
|
||||||
|
&resolution.into(),
|
||||||
|
settings.as_ref().into(),
|
||||||
|
&state,
|
||||||
|
Box::new(DefaultInstallLogger),
|
||||||
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
(environment, UpgradeOutcome::UpgradeEnvironment)
|
||||||
|
} else {
|
||||||
|
// Otherwise, upgrade the existing environment.
|
||||||
|
// TODO(zanieb): Build the environment in the cache directory then copy into the tool
|
||||||
|
// directory.
|
||||||
|
let EnvironmentUpdate {
|
||||||
|
environment,
|
||||||
|
changelog,
|
||||||
|
} = update_environment(
|
||||||
|
environment,
|
||||||
|
spec,
|
||||||
|
&settings,
|
||||||
|
&state,
|
||||||
|
Box::new(SummaryResolveLogger),
|
||||||
|
Box::new(UpgradeInstallLogger::new(name.clone())),
|
||||||
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let outcome = if changelog.includes(name) {
|
||||||
|
UpgradeOutcome::UpgradeTool
|
||||||
|
} else if changelog.is_empty() {
|
||||||
|
UpgradeOutcome::NoOp
|
||||||
|
} else {
|
||||||
|
UpgradeOutcome::UpgradeDependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
(environment, outcome)
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
outcome,
|
||||||
|
UpgradeOutcome::UpgradeEnvironment | UpgradeOutcome::UpgradeTool
|
||||||
|
) {
|
||||||
// At this point, we updated the existing environment, so we should remove any of its
|
// At this point, we updated the existing environment, so we should remove any of its
|
||||||
// existing executables.
|
// existing executables.
|
||||||
remove_entrypoints(&existing_tool_receipt);
|
remove_entrypoints(&existing_tool_receipt);
|
||||||
|
|
||||||
|
// If we modified the target tool, reinstall the entrypoints.
|
||||||
install_executables(
|
install_executables(
|
||||||
&environment,
|
&environment,
|
||||||
name,
|
name,
|
||||||
|
@ -208,5 +336,32 @@ async fn upgrade_tool(
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(changelog)
|
Ok(outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a list of names, return a conjunction of the names (e.g., "Alice, Bob and Charlie").
|
||||||
|
fn conjunction(names: Vec<String>) -> String {
|
||||||
|
let mut names = names.into_iter();
|
||||||
|
let first = names.next();
|
||||||
|
let last = names.next_back();
|
||||||
|
match (first, last) {
|
||||||
|
(Some(first), Some(last)) => {
|
||||||
|
let mut result = first;
|
||||||
|
let mut comma = false;
|
||||||
|
for name in names {
|
||||||
|
result.push_str(", ");
|
||||||
|
result.push_str(&name);
|
||||||
|
comma = true;
|
||||||
|
}
|
||||||
|
if comma {
|
||||||
|
result.push_str(", and ");
|
||||||
|
} else {
|
||||||
|
result.push_str(" and ");
|
||||||
|
}
|
||||||
|
result.push_str(&last);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
(Some(first), None) => first,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -940,9 +940,12 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
|
|
||||||
commands::tool_upgrade(
|
commands::tool_upgrade(
|
||||||
args.name,
|
args.name,
|
||||||
|
args.python,
|
||||||
globals.connectivity,
|
globals.connectivity,
|
||||||
args.args,
|
args.args,
|
||||||
args.filesystem,
|
args.filesystem,
|
||||||
|
globals.python_preference,
|
||||||
|
globals.python_downloads,
|
||||||
globals.concurrency,
|
globals.concurrency,
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
&cache,
|
&cache,
|
||||||
|
|
|
@ -432,6 +432,7 @@ impl ToolInstallSettings {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ToolUpgradeSettings {
|
pub(crate) struct ToolUpgradeSettings {
|
||||||
pub(crate) name: Vec<PackageName>,
|
pub(crate) name: Vec<PackageName>,
|
||||||
|
pub(crate) python: Option<String>,
|
||||||
pub(crate) args: ResolverInstallerOptions,
|
pub(crate) args: ResolverInstallerOptions,
|
||||||
pub(crate) filesystem: ResolverInstallerOptions,
|
pub(crate) filesystem: ResolverInstallerOptions,
|
||||||
}
|
}
|
||||||
|
@ -442,6 +443,7 @@ impl ToolUpgradeSettings {
|
||||||
pub(crate) fn resolve(args: ToolUpgradeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
pub(crate) fn resolve(args: ToolUpgradeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||||
let ToolUpgradeArgs {
|
let ToolUpgradeArgs {
|
||||||
name,
|
name,
|
||||||
|
python,
|
||||||
all,
|
all,
|
||||||
mut installer,
|
mut installer,
|
||||||
build,
|
build,
|
||||||
|
@ -463,6 +465,7 @@ impl ToolUpgradeSettings {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: if all { vec![] } else { name },
|
name: if all { vec![] } else { name },
|
||||||
|
python,
|
||||||
args,
|
args,
|
||||||
filesystem,
|
filesystem,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use assert_fs::prelude::*;
|
use assert_fs::prelude::*;
|
||||||
|
|
||||||
use common::{uv_snapshot, TestContext};
|
use common::{uv_snapshot, TestContext};
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
@ -577,3 +578,159 @@ fn test_tool_upgrade_with() {
|
||||||
+ pytz==2024.1
|
+ pytz==2024.1
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tool_upgrade_python() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.11", "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");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("babel==2.6.0")
|
||||||
|
.arg("--index-url")
|
||||||
|
.arg("https://test.pypi.org/simple/")
|
||||||
|
.arg("--python").arg("3.11")
|
||||||
|
.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: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ babel==2.6.0
|
||||||
|
+ pytz==2018.5
|
||||||
|
Installed 1 executable: pybabel
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(
|
||||||
|
context.filters(),
|
||||||
|
context.tool_upgrade().arg("babel")
|
||||||
|
.arg("--python").arg("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: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ babel==2.6.0
|
||||||
|
+ pytz==2018.5
|
||||||
|
Installed 1 executable: pybabel
|
||||||
|
Upgraded tool environment for `babel` to Python 3.12
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
let content = fs_err::read_to_string(tool_dir.join("babel").join("pyvenv.cfg")).unwrap();
|
||||||
|
let lines: Vec<&str> = content.split('\n').collect();
|
||||||
|
assert_snapshot!(lines[lines.len() - 3], @r###"
|
||||||
|
version_info = 3.12.[X]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tool_upgrade_python_with_all() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.11", "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");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("babel==2.6.0")
|
||||||
|
.arg("--index-url")
|
||||||
|
.arg("https://test.pypi.org/simple/")
|
||||||
|
.arg("--python").arg("3.11")
|
||||||
|
.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: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ babel==2.6.0
|
||||||
|
+ pytz==2018.5
|
||||||
|
Installed 1 executable: pybabel
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("python-dotenv")
|
||||||
|
.arg("--index-url")
|
||||||
|
.arg("https://test.pypi.org/simple/")
|
||||||
|
.arg("--python").arg("3.11")
|
||||||
|
.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: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ python-dotenv==0.10.2.post2
|
||||||
|
Installed 1 executable: dotenv
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(
|
||||||
|
context.filters(),
|
||||||
|
context.tool_upgrade().arg("--all")
|
||||||
|
.arg("--python").arg("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: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ babel==2.6.0
|
||||||
|
+ pytz==2018.5
|
||||||
|
Installed 1 executable: pybabel
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ python-dotenv==0.10.2.post2
|
||||||
|
Installed 1 executable: dotenv
|
||||||
|
Upgraded tool environments for `babel` and `python-dotenv` to Python 3.12
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
let content = fs_err::read_to_string(tool_dir.join("babel").join("pyvenv.cfg")).unwrap();
|
||||||
|
let lines: Vec<&str> = content.split('\n').collect();
|
||||||
|
assert_snapshot!(lines[lines.len() - 3], @r###"
|
||||||
|
version_info = 3.12.[X]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
let content = fs_err::read_to_string(tool_dir.join("python-dotenv").join("pyvenv.cfg")).unwrap();
|
||||||
|
let lines: Vec<&str> = content.split('\n').collect();
|
||||||
|
assert_snapshot!(lines[lines.len() - 3], @r###"
|
||||||
|
version_info = 3.12.[X]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -3248,6 +3248,11 @@ uv tool upgrade [OPTIONS] <NAME>...
|
||||||
|
|
||||||
<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>
|
<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>
|
||||||
|
|
||||||
|
</dd><dt><code>--python</code>, <code>-p</code> <i>python</i></dt><dd><p>Upgrade a tool, and specify it to use the given Python interpreter to build its environment. Use with <code>--all</code> to apply to all tools.</p>
|
||||||
|
|
||||||
|
<p>See <a href="#uv-python">uv python</a> for details on Python discovery and supported request formats.</p>
|
||||||
|
|
||||||
|
<p>May also be set with the <code>UV_PYTHON</code> environment variable.</p>
|
||||||
</dd><dt><code>--python-preference</code> <i>python-preference</i></dt><dd><p>Whether to prefer uv-managed or system Python installations.</p>
|
</dd><dt><code>--python-preference</code> <i>python-preference</i></dt><dd><p>Whether to prefer uv-managed or system Python installations.</p>
|
||||||
|
|
||||||
<p>By default, uv prefers using Python versions it manages. However, it will use system Python installations if a uv-managed Python is not installed. This option allows prioritizing or ignoring system Python installations.</p>
|
<p>By default, uv prefers using Python versions it manages. However, it will use system Python installations if a uv-managed Python is not installed. This option allows prioritizing or ignoring system Python installations.</p>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue