mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +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",
|
||||
"reqwest",
|
||||
"rustc-hash",
|
||||
"same-file",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar",
|
||||
|
|
|
@ -3396,6 +3396,20 @@ pub struct ToolUpgradeArgs {
|
|||
#[arg(long, conflicts_with("name"))]
|
||||
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)]
|
||||
pub installer: ResolverInstallerArgs,
|
||||
|
||||
|
|
|
@ -300,4 +300,26 @@ impl PythonEnvironment {
|
|||
pub fn into_interpreter(self) -> 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 }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
same-file = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
|
|
@ -277,23 +277,7 @@ pub(crate) async fn install(
|
|||
installed_tools
|
||||
.get_environment(&from.name, &cache)?
|
||||
.filter(|environment| {
|
||||
// TODO(zanieb): Consider using `sysconfig.get_path("stdlib")` instead, which
|
||||
// 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 {
|
||||
if environment.uses(&interpreter) {
|
||||
trace!(
|
||||
"Existing interpreter matches the requested interpreter for `{}`: {}",
|
||||
from.name,
|
||||
|
|
|
@ -5,16 +5,24 @@ use owo_colors::OwoColorize;
|
|||
use tracing::debug;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::Concurrency;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, Interpreter, PythonDownloads, PythonInstallation, PythonPreference,
|
||||
PythonRequest,
|
||||
};
|
||||
use uv_requirements::RequirementsSpecification;
|
||||
use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions};
|
||||
use uv_tool::InstalledTools;
|
||||
|
||||
use crate::commands::pip::loggers::{SummaryResolveLogger, UpgradeInstallLogger};
|
||||
use crate::commands::pip::operations::Changelog;
|
||||
use crate::commands::project::{update_environment, EnvironmentUpdate};
|
||||
use crate::commands::pip::loggers::{
|
||||
DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger,
|
||||
};
|
||||
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::install_executables, ExitStatus, SharedState};
|
||||
use crate::printer::Printer;
|
||||
|
@ -23,9 +31,12 @@ use crate::settings::ResolverInstallerSettings;
|
|||
/// Upgrade a tool.
|
||||
pub(crate) async fn upgrade(
|
||||
name: Vec<PackageName>,
|
||||
python: Option<String>,
|
||||
connectivity: Connectivity,
|
||||
args: ResolverInstallerOptions,
|
||||
filesystem: ResolverInstallerOptions,
|
||||
python_preference: PythonPreference,
|
||||
python_downloads: PythonDownloads,
|
||||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
cache: &Cache,
|
||||
|
@ -34,6 +45,7 @@ pub(crate) async fn upgrade(
|
|||
let installed_tools = InstalledTools::from_settings()?.init()?;
|
||||
let _lock = installed_tools.lock().await?;
|
||||
|
||||
// Collect the tools to upgrade.
|
||||
let names: BTreeSet<PackageName> = {
|
||||
if name.is_empty() {
|
||||
installed_tools
|
||||
|
@ -52,16 +64,45 @@ pub(crate) async fn upgrade(
|
|||
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.
|
||||
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.
|
||||
let mut failed_upgrade = false;
|
||||
|
||||
for name in &names {
|
||||
debug!("Upgrading tool: `{name}`");
|
||||
let changelog = upgrade_tool(
|
||||
let result = upgrade_tool(
|
||||
name,
|
||||
interpreter.as_ref(),
|
||||
printer,
|
||||
&installed_tools,
|
||||
&args,
|
||||
|
@ -73,9 +114,15 @@ pub(crate) async fn upgrade(
|
|||
)
|
||||
.await;
|
||||
|
||||
match changelog {
|
||||
Ok(changelog) => {
|
||||
did_upgrade |= !changelog.is_empty();
|
||||
match result {
|
||||
Ok(UpgradeOutcome::UpgradeEnvironment) => {
|
||||
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) => {
|
||||
// If we have a single tool, return the error directly.
|
||||
|
@ -97,15 +144,43 @@ pub(crate) async fn upgrade(
|
|||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
if !did_upgrade {
|
||||
if did_upgrade_tool.is_empty() && did_upgrade_environment.is_empty() {
|
||||
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)
|
||||
}
|
||||
|
||||
#[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(
|
||||
name: &PackageName,
|
||||
interpreter: Option<&Interpreter>,
|
||||
printer: Printer,
|
||||
installed_tools: &InstalledTools,
|
||||
args: &ResolverInstallerOptions,
|
||||
|
@ -114,7 +189,7 @@ async fn upgrade_tool(
|
|||
connectivity: Connectivity,
|
||||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
) -> Result<Changelog> {
|
||||
) -> Result<UpgradeOutcome> {
|
||||
// Ensure the tool is installed.
|
||||
let existing_tool_receipt = match installed_tools.get_tool_receipt(name) {
|
||||
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(None) => {
|
||||
let install_command = format!("uv tool install {name}");
|
||||
|
@ -170,32 +245,85 @@ async fn upgrade_tool(
|
|||
// Initialize any shared state.
|
||||
let state = SharedState::default();
|
||||
|
||||
// TODO(zanieb): Build the environment in the cache directory then copy into the tool
|
||||
// directory.
|
||||
let EnvironmentUpdate {
|
||||
environment,
|
||||
changelog,
|
||||
} = update_environment(
|
||||
existing_environment,
|
||||
spec,
|
||||
&settings,
|
||||
&state,
|
||||
Box::new(SummaryResolveLogger),
|
||||
Box::new(UpgradeInstallLogger::new(name.clone())),
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
// Check if we need to create a new environment — if so, resolve it first, then
|
||||
// install the requested tool
|
||||
let (environment, outcome) = if let Some(interpreter) =
|
||||
interpreter.filter(|interpreter| !environment.uses(interpreter))
|
||||
{
|
||||
// If we're using a new interpreter, re-create the environment for each tool.
|
||||
let resolution = resolve_environment(
|
||||
RequirementsSpecification::from_requirements(requirements.to_vec()).into(),
|
||||
interpreter,
|
||||
settings.as_ref().into(),
|
||||
&state,
|
||||
Box::new(SummaryResolveLogger),
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If we modified the target tool, reinstall the entrypoints.
|
||||
if changelog.includes(name) {
|
||||
let environment = installed_tools.create_environment(name, interpreter.clone())?;
|
||||
|
||||
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
|
||||
// existing executables.
|
||||
remove_entrypoints(&existing_tool_receipt);
|
||||
|
||||
// If we modified the target tool, reinstall the entrypoints.
|
||||
install_executables(
|
||||
&environment,
|
||||
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(
|
||||
args.name,
|
||||
args.python,
|
||||
globals.connectivity,
|
||||
args.args,
|
||||
args.filesystem,
|
||||
globals.python_preference,
|
||||
globals.python_downloads,
|
||||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
&cache,
|
||||
|
|
|
@ -432,6 +432,7 @@ impl ToolInstallSettings {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolUpgradeSettings {
|
||||
pub(crate) name: Vec<PackageName>,
|
||||
pub(crate) python: Option<String>,
|
||||
pub(crate) args: ResolverInstallerOptions,
|
||||
pub(crate) filesystem: ResolverInstallerOptions,
|
||||
}
|
||||
|
@ -442,6 +443,7 @@ impl ToolUpgradeSettings {
|
|||
pub(crate) fn resolve(args: ToolUpgradeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let ToolUpgradeArgs {
|
||||
name,
|
||||
python,
|
||||
all,
|
||||
mut installer,
|
||||
build,
|
||||
|
@ -463,6 +465,7 @@ impl ToolUpgradeSettings {
|
|||
|
||||
Self {
|
||||
name: if all { vec![] } else { name },
|
||||
python,
|
||||
args,
|
||||
filesystem,
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use assert_fs::prelude::*;
|
||||
|
||||
use common::{uv_snapshot, TestContext};
|
||||
use insta::assert_snapshot;
|
||||
|
||||
mod common;
|
||||
|
||||
|
@ -577,3 +578,159 @@ fn test_tool_upgrade_with() {
|
|||
+ 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>
|
||||
|
||||
</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>
|
||||
|
||||
<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