mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Respect tool upgrades in uv tool install
(#4736)
## Summary For now the semantics are such that if the requested requirements from the command line don't match the receipt (or if any `--reinstall` or `--upgrade` is requested), we proceed with an install, passing the `--reinstall` and `--upgrade` to the underlying Python environment. This may lead to some unintuitive behaviors, but it's simplest for now. For example: - `uv tool install black<24` followed by `uv tool install black --upgrade` will install the latest version of `black`, removing the `<24` constraint. - `uv tool install black --with black-plugin` followed by `uv tool install black` will remove `black-plugin`. Closes https://github.com/astral-sh/uv/issues/4659.
This commit is contained in:
parent
21187e1f36
commit
32dc9bef59
10 changed files with 293 additions and 62 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5039,6 +5039,7 @@ dependencies = [
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
|
"pypi-types",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
@ -5,7 +5,7 @@ use url::Url;
|
||||||
|
|
||||||
use pep440_rs::VersionSpecifiers;
|
use pep440_rs::VersionSpecifiers;
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
|
use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
|
||||||
use uv_git::{GitReference, GitSha};
|
use uv_git::{GitReference, GitSha, GitUrl};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -66,6 +66,80 @@ impl From<Requirement> for pep508_rs::Requirement<VerbatimUrl> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Requirement> for pep508_rs::Requirement<VerbatimParsedUrl> {
|
||||||
|
/// Convert a [`Requirement`] to a [`pep508_rs::Requirement`].
|
||||||
|
fn from(requirement: Requirement) -> Self {
|
||||||
|
pep508_rs::Requirement {
|
||||||
|
name: requirement.name,
|
||||||
|
extras: requirement.extras,
|
||||||
|
marker: requirement.marker,
|
||||||
|
origin: requirement.origin,
|
||||||
|
version_or_url: match requirement.source {
|
||||||
|
RequirementSource::Registry { specifier, .. } => {
|
||||||
|
Some(VersionOrUrl::VersionSpecifier(specifier))
|
||||||
|
}
|
||||||
|
RequirementSource::Url {
|
||||||
|
subdirectory,
|
||||||
|
location,
|
||||||
|
url,
|
||||||
|
} => Some(VersionOrUrl::Url(VerbatimParsedUrl {
|
||||||
|
parsed_url: ParsedUrl::Archive(ParsedArchiveUrl {
|
||||||
|
url: location,
|
||||||
|
subdirectory,
|
||||||
|
}),
|
||||||
|
verbatim: url,
|
||||||
|
})),
|
||||||
|
RequirementSource::Git {
|
||||||
|
repository,
|
||||||
|
reference,
|
||||||
|
precise,
|
||||||
|
subdirectory,
|
||||||
|
url,
|
||||||
|
} => {
|
||||||
|
let git_url = if let Some(precise) = precise {
|
||||||
|
GitUrl::new(repository, reference).with_precise(precise)
|
||||||
|
} else {
|
||||||
|
GitUrl::new(repository, reference)
|
||||||
|
};
|
||||||
|
Some(VersionOrUrl::Url(VerbatimParsedUrl {
|
||||||
|
parsed_url: ParsedUrl::Git(ParsedGitUrl {
|
||||||
|
url: git_url,
|
||||||
|
subdirectory,
|
||||||
|
}),
|
||||||
|
verbatim: url,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
RequirementSource::Path {
|
||||||
|
install_path,
|
||||||
|
lock_path,
|
||||||
|
url,
|
||||||
|
} => Some(VersionOrUrl::Url(VerbatimParsedUrl {
|
||||||
|
parsed_url: ParsedUrl::Path(ParsedPathUrl {
|
||||||
|
url: url.to_url(),
|
||||||
|
install_path,
|
||||||
|
lock_path,
|
||||||
|
}),
|
||||||
|
verbatim: url,
|
||||||
|
})),
|
||||||
|
RequirementSource::Directory {
|
||||||
|
install_path,
|
||||||
|
lock_path,
|
||||||
|
editable,
|
||||||
|
url,
|
||||||
|
} => Some(VersionOrUrl::Url(VerbatimParsedUrl {
|
||||||
|
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
|
||||||
|
url: url.to_url(),
|
||||||
|
install_path,
|
||||||
|
lock_path,
|
||||||
|
editable,
|
||||||
|
}),
|
||||||
|
verbatim: url,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<pep508_rs::Requirement<VerbatimParsedUrl>> for Requirement {
|
impl From<pep508_rs::Requirement<VerbatimParsedUrl>> for Requirement {
|
||||||
/// Convert a [`pep508_rs::Requirement`] to a [`Requirement`].
|
/// Convert a [`pep508_rs::Requirement`] to a [`Requirement`].
|
||||||
fn from(requirement: pep508_rs::Requirement<VerbatimParsedUrl>) -> Self {
|
fn from(requirement: pep508_rs::Requirement<VerbatimParsedUrl>) -> Self {
|
||||||
|
|
|
@ -16,6 +16,7 @@ workspace = true
|
||||||
install-wheel-rs = { workspace = true }
|
install-wheel-rs = { workspace = true }
|
||||||
pep440_rs = { workspace = true }
|
pep440_rs = { workspace = true }
|
||||||
pep508_rs = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
|
pypi-types = { workspace = true }
|
||||||
uv-cache = { workspace = true }
|
uv-cache = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
uv-state = { workspace = true }
|
uv-state = { workspace = true }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use path_slash::PathBufExt;
|
use path_slash::PathBufExt;
|
||||||
|
use pypi_types::VerbatimParsedUrl;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use toml_edit::value;
|
use toml_edit::value;
|
||||||
use toml_edit::Array;
|
use toml_edit::Array;
|
||||||
|
@ -14,7 +15,7 @@ use toml_edit::Value;
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct Tool {
|
pub struct Tool {
|
||||||
/// The requirements requested by the user during installation.
|
/// The requirements requested by the user during installation.
|
||||||
requirements: Vec<pep508_rs::Requirement>,
|
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
||||||
/// The Python requested by the user during installation.
|
/// The Python requested by the user during installation.
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
/// A mapping of entry point names to their metadata.
|
/// A mapping of entry point names to their metadata.
|
||||||
|
@ -58,7 +59,7 @@ fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value
|
||||||
impl Tool {
|
impl Tool {
|
||||||
/// Create a new `Tool`.
|
/// Create a new `Tool`.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
requirements: Vec<pep508_rs::Requirement>,
|
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
entrypoints: impl Iterator<Item = ToolEntrypoint>,
|
entrypoints: impl Iterator<Item = ToolEntrypoint>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -108,6 +109,10 @@ impl Tool {
|
||||||
pub fn entrypoints(&self) -> &[ToolEntrypoint] {
|
pub fn entrypoints(&self) -> &[ToolEntrypoint] {
|
||||||
&self.entrypoints
|
&self.entrypoints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn requirements(&self) -> &[pep508_rs::Requirement<VerbatimParsedUrl>] {
|
||||||
|
&self.requirements
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolEntrypoint {
|
impl ToolEntrypoint {
|
||||||
|
|
|
@ -24,6 +24,7 @@ use uv_toolchain::{
|
||||||
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
|
|
||||||
use crate::commands::pip;
|
use crate::commands::pip;
|
||||||
|
use crate::commands::pip::operations::Modifications;
|
||||||
use crate::commands::reporters::ResolverReporter;
|
use crate::commands::reporters::ResolverReporter;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::ResolverInstallerSettings;
|
use crate::settings::ResolverInstallerSettings;
|
||||||
|
@ -375,6 +376,7 @@ pub(crate) async fn resolve_names(
|
||||||
pub(crate) async fn update_environment(
|
pub(crate) async fn update_environment(
|
||||||
venv: PythonEnvironment,
|
venv: PythonEnvironment,
|
||||||
spec: RequirementsSpecification,
|
spec: RequirementsSpecification,
|
||||||
|
modifications: Modifications,
|
||||||
settings: &ResolverInstallerSettings,
|
settings: &ResolverInstallerSettings,
|
||||||
state: &SharedState,
|
state: &SharedState,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
|
@ -402,7 +404,7 @@ pub(crate) async fn update_environment(
|
||||||
|
|
||||||
// Check if the current environment satisfies the requirements
|
// Check if the current environment satisfies the requirements
|
||||||
let site_packages = SitePackages::from_environment(&venv)?;
|
let site_packages = SitePackages::from_environment(&venv)?;
|
||||||
if spec.source_trees.is_empty() && reinstall.is_none() {
|
if spec.source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() {
|
||||||
match site_packages.satisfies(&spec.requirements, &spec.constraints)? {
|
match site_packages.satisfies(&spec.requirements, &spec.constraints)? {
|
||||||
// If the requirements are already satisfied, we're done.
|
// If the requirements are already satisfied, we're done.
|
||||||
SatisfiesResult::Fresh {
|
SatisfiesResult::Fresh {
|
||||||
|
@ -554,7 +556,7 @@ pub(crate) async fn update_environment(
|
||||||
pip::operations::install(
|
pip::operations::install(
|
||||||
&resolution,
|
&resolution,
|
||||||
site_packages,
|
site_packages,
|
||||||
pip::operations::Modifications::Sufficient,
|
modifications,
|
||||||
reinstall,
|
reinstall,
|
||||||
build_options,
|
build_options,
|
||||||
*link_mode,
|
*link_mode,
|
||||||
|
|
|
@ -111,6 +111,7 @@ pub(crate) async fn run(
|
||||||
let environment = project::update_environment(
|
let environment = project::update_environment(
|
||||||
venv,
|
venv,
|
||||||
spec,
|
spec,
|
||||||
|
Modifications::Sufficient,
|
||||||
&settings,
|
&settings,
|
||||||
&state,
|
&state,
|
||||||
preview,
|
preview,
|
||||||
|
@ -300,6 +301,7 @@ pub(crate) async fn run(
|
||||||
project::update_environment(
|
project::update_environment(
|
||||||
venv,
|
venv,
|
||||||
spec,
|
spec,
|
||||||
|
Modifications::Sufficient,
|
||||||
&settings,
|
&settings,
|
||||||
&state,
|
&state,
|
||||||
preview,
|
preview,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use distribution_types::Name;
|
||||||
use pypi_types::Requirement;
|
use pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::{Concurrency, PreviewMode, Reinstall};
|
use uv_configuration::{Concurrency, PreviewMode};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use uv_fs::replace_symlink;
|
use uv_fs::replace_symlink;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
|
@ -25,6 +25,7 @@ use uv_toolchain::{
|
||||||
};
|
};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
use crate::commands::pip::operations::Modifications;
|
||||||
use crate::commands::project::{update_environment, SharedState};
|
use crate::commands::project::{update_environment, SharedState};
|
||||||
use crate::commands::{project, ExitStatus};
|
use crate::commands::{project, ExitStatus};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
@ -122,37 +123,6 @@ pub(crate) async fn install(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let installed_tools = InstalledTools::from_settings()?;
|
|
||||||
|
|
||||||
let existing_tool_receipt = installed_tools.get_tool_receipt(&from.name)?;
|
|
||||||
// TODO(zanieb): Automatically replace an existing tool if the request differs
|
|
||||||
let reinstall_entry_points = if existing_tool_receipt.is_some() {
|
|
||||||
if force {
|
|
||||||
debug!("Replacing existing tool due to `--force` flag.");
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
match settings.reinstall {
|
|
||||||
Reinstall::All => {
|
|
||||||
debug!("Replacing existing tool due to `--reinstall` flag.");
|
|
||||||
true
|
|
||||||
}
|
|
||||||
// Do not replace the entry points unless the tool is explicitly requested
|
|
||||||
Reinstall::Packages(ref packages) => packages.contains(&from.name),
|
|
||||||
// If not reinstalling... then we're done
|
|
||||||
Reinstall::None => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"Tool `{}` is already installed",
|
|
||||||
from.name
|
|
||||||
)?;
|
|
||||||
return Ok(ExitStatus::Failure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Combine the `from` and `with` requirements.
|
// Combine 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());
|
||||||
|
@ -175,23 +145,44 @@ pub(crate) async fn install(
|
||||||
requirements
|
requirements
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let installed_tools = InstalledTools::from_settings()?;
|
||||||
|
let existing_tool_receipt = installed_tools.get_tool_receipt(&from.name)?;
|
||||||
|
|
||||||
|
// If the requested and receipt requirements are the same...
|
||||||
|
if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
|
||||||
|
let receipt = tool_receipt
|
||||||
|
.requirements()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(Requirement::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if requirements == receipt {
|
||||||
|
// And the user didn't request a reinstall or upgrade...
|
||||||
|
if !force && settings.reinstall.is_none() && settings.upgrade.is_none() {
|
||||||
|
// We're done.
|
||||||
|
writeln!(printer.stderr(), "Tool `{from}` is already installed")?;
|
||||||
|
return Ok(ExitStatus::Failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace entrypoints if the tool already exists (and we made it this far). If we find existing
|
||||||
|
// entrypoints later on, and the tool _doesn't_ exist, we'll avoid removing the external tool's
|
||||||
|
// entrypoints (without `--force`).
|
||||||
|
let reinstall_entry_points = existing_tool_receipt.is_some();
|
||||||
|
|
||||||
// TODO(zanieb): Build the environment in the cache directory then copy into the tool directory
|
// TODO(zanieb): Build the environment in the cache directory then copy into the tool directory
|
||||||
// This lets us confirm the environment is valid before removing an existing install
|
// This lets us confirm the environment is valid before removing an existing install
|
||||||
let environment = installed_tools.environment(
|
let environment = installed_tools.environment(&from.name, force, interpreter, cache)?;
|
||||||
&from.name,
|
|
||||||
// Do not remove the existing environment if we're reinstalling a subset of packages
|
|
||||||
!matches!(settings.reinstall, Reinstall::Packages(_)),
|
|
||||||
interpreter,
|
|
||||||
cache,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Install the ephemeral requirements.
|
// Install the ephemeral requirements.
|
||||||
let spec = RequirementsSpecification::from_requirements(requirements.clone());
|
let spec = RequirementsSpecification::from_requirements(requirements.clone());
|
||||||
let environment = update_environment(
|
let environment = update_environment(
|
||||||
environment,
|
environment,
|
||||||
spec,
|
spec,
|
||||||
|
Modifications::Exact,
|
||||||
&settings,
|
&settings,
|
||||||
&SharedState::default(),
|
&state,
|
||||||
preview,
|
preview,
|
||||||
connectivity,
|
connectivity,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
@ -207,17 +198,6 @@ pub(crate) async fn install(
|
||||||
bail!("Expected at least one requirement")
|
bail!("Expected at least one requirement")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exit early if we're not supposed to be reinstalling entry points
|
|
||||||
// e.g. `--reinstall-package` was used for some dependency
|
|
||||||
if existing_tool_receipt.is_some() && !reinstall_entry_points {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"Updated environment for tool `{}`",
|
|
||||||
from.name
|
|
||||||
)?;
|
|
||||||
return Ok(ExitStatus::Success);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a suitable path to install into
|
// Find a suitable path to install into
|
||||||
// TODO(zanieb): Warn if this directory is not on the PATH
|
// TODO(zanieb): Warn if this directory is not on the PATH
|
||||||
let executable_directory = find_executable_directory()?;
|
let executable_directory = find_executable_directory()?;
|
||||||
|
@ -324,7 +304,7 @@ pub(crate) async fn install(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve any [`UnnamedRequirements`].
|
/// Resolve any [`UnnamedRequirements`].
|
||||||
pub(crate) async fn resolve_requirements(
|
async fn resolve_requirements(
|
||||||
requirements: impl Iterator<Item = &str>,
|
requirements: impl Iterator<Item = &str>,
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
settings: &ResolverInstallerSettings,
|
settings: &ResolverInstallerSettings,
|
||||||
|
|
|
@ -21,6 +21,7 @@ use uv_toolchain::{
|
||||||
};
|
};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
use crate::commands::pip::operations::Modifications;
|
||||||
use crate::commands::project::{update_environment, SharedState};
|
use crate::commands::project::{update_environment, SharedState};
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
@ -103,6 +104,7 @@ pub(crate) async fn run(
|
||||||
update_environment(
|
update_environment(
|
||||||
venv,
|
venv,
|
||||||
spec,
|
spec,
|
||||||
|
Modifications::Sufficient,
|
||||||
&settings,
|
&settings,
|
||||||
&SharedState::default(),
|
&SharedState::default(),
|
||||||
preview,
|
preview,
|
||||||
|
|
|
@ -792,9 +792,9 @@ pub fn run_and_format<T: AsRef<str>>(
|
||||||
// The optional leading +/- is for install logs, the optional next line is for lock files
|
// The optional leading +/- is for install logs, the optional next line is for lock files
|
||||||
let windows_only_deps = [
|
let windows_only_deps = [
|
||||||
("( [+-] )?colorama==\\d+(\\.[\\d+])+\n( # via .*\n)?"),
|
("( [+-] )?colorama==\\d+(\\.[\\d+])+\n( # via .*\n)?"),
|
||||||
("( [+-] )?colorama==\\d+(\\.[\\d+])+\\s+(# via .*\n)?"),
|
("( [+-] )?colorama==\\d+(\\.[\\d+])+(\\s+# via .*)?\n"),
|
||||||
("( [+-] )?tzdata==\\d+(\\.[\\d+])+\n( # via .*\n)?"),
|
("( [+-] )?tzdata==\\d+(\\.[\\d+])+\n( # via .*\n)?"),
|
||||||
("( [+-] )?tzdata==\\d+(\\.[\\d+])+\\s+(# via .*\n)?"),
|
("( [+-] )?tzdata==\\d+(\\.[\\d+])+(\\s+# via .*)?\n"),
|
||||||
];
|
];
|
||||||
let mut removed_packages = 0;
|
let mut removed_packages = 0;
|
||||||
for windows_only_dep in windows_only_deps {
|
for windows_only_dep in windows_only_deps {
|
||||||
|
|
|
@ -436,12 +436,19 @@ fn tool_install_already_installed() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
warning: `uv tool install` is experimental and may change without warning.
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
Resolved [N] packages in [TIME]
|
Resolved [N] packages in [TIME]
|
||||||
|
Uninstalled [N] packages in [TIME]
|
||||||
Installed [N] packages in [TIME]
|
Installed [N] packages in [TIME]
|
||||||
|
- black==24.3.0
|
||||||
+ black==24.3.0
|
+ black==24.3.0
|
||||||
|
- click==8.1.7
|
||||||
+ click==8.1.7
|
+ click==8.1.7
|
||||||
|
- mypy-extensions==1.0.0
|
||||||
+ mypy-extensions==1.0.0
|
+ mypy-extensions==1.0.0
|
||||||
|
- packaging==24.0
|
||||||
+ packaging==24.0
|
+ packaging==24.0
|
||||||
|
- pathspec==0.12.1
|
||||||
+ pathspec==0.12.1
|
+ pathspec==0.12.1
|
||||||
|
- platformdirs==4.2.0
|
||||||
+ platformdirs==4.2.0
|
+ platformdirs==4.2.0
|
||||||
Installed: black, blackd
|
Installed: black, blackd
|
||||||
"###);
|
"###);
|
||||||
|
@ -469,7 +476,7 @@ fn tool_install_already_installed() {
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// Install `black` again with `--reinstall-package` for a dependency
|
// Install `black` again with `--reinstall-package` for a dependency
|
||||||
// We should reinstall `click` in the environment but not reinstall the entry points
|
// We should reinstall `click` in the environment but not reinstall `black`
|
||||||
uv_snapshot!(context.filters(), context.tool_install()
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
.arg("black")
|
.arg("black")
|
||||||
.arg("--reinstall-package")
|
.arg("--reinstall-package")
|
||||||
|
@ -487,7 +494,7 @@ fn tool_install_already_installed() {
|
||||||
Installed [N] packages in [TIME]
|
Installed [N] packages in [TIME]
|
||||||
- click==8.1.7
|
- click==8.1.7
|
||||||
+ click==8.1.7
|
+ click==8.1.7
|
||||||
Updated environment for tool `black`
|
Installed: black, blackd
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -683,12 +690,19 @@ fn tool_install_entry_point_exists() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
warning: `uv tool install` is experimental and may change without warning.
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
Resolved [N] packages in [TIME]
|
Resolved [N] packages in [TIME]
|
||||||
|
Uninstalled [N] packages in [TIME]
|
||||||
Installed [N] packages in [TIME]
|
Installed [N] packages in [TIME]
|
||||||
|
- black==24.3.0
|
||||||
+ black==24.3.0
|
+ black==24.3.0
|
||||||
|
- click==8.1.7
|
||||||
+ click==8.1.7
|
+ click==8.1.7
|
||||||
|
- mypy-extensions==1.0.0
|
||||||
+ mypy-extensions==1.0.0
|
+ mypy-extensions==1.0.0
|
||||||
|
- packaging==24.0
|
||||||
+ packaging==24.0
|
+ packaging==24.0
|
||||||
|
- pathspec==0.12.1
|
||||||
+ pathspec==0.12.1
|
+ pathspec==0.12.1
|
||||||
|
- platformdirs==4.2.0
|
||||||
+ platformdirs==4.2.0
|
+ platformdirs==4.2.0
|
||||||
Installed: black, blackd
|
Installed: black, blackd
|
||||||
"###);
|
"###);
|
||||||
|
@ -716,7 +730,7 @@ fn tool_install_entry_point_exists() {
|
||||||
}, {
|
}, {
|
||||||
// Should run black in the virtual environment
|
// Should run black in the virtual environment
|
||||||
assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###"
|
assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###"
|
||||||
#![TEMP_DIR]/tools/black/bin/python
|
#![TEMP_DIR]/tools/black/bin/python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
@ -1165,3 +1179,153 @@ fn tool_install_unnamed_with() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test upgrading an already installed tool.
|
||||||
|
#[test]
|
||||||
|
fn tool_install_upgrade() {
|
||||||
|
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 `black`.
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black==24.1.1")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ black==24.1.1
|
||||||
|
+ click==8.1.7
|
||||||
|
+ mypy-extensions==1.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pathspec==0.12.1
|
||||||
|
+ platformdirs==4.2.0
|
||||||
|
Installed: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = ["black==24.1.1"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install without the constraint. It should be replaced, but the package shouldn't be installed
|
||||||
|
// since it's already satisfied in the environment.
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Installed: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = ["black"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install with a `with`. It should be added to the environment.
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.arg("--with")
|
||||||
|
.arg("iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
|
||||||
|
Installed: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = [
|
||||||
|
"black",
|
||||||
|
"iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
|
||||||
|
]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install with `--upgrade`. `black` should be reinstalled with a more recent version, and
|
||||||
|
// `iniconfig` should be removed.
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.arg("--upgrade")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Uninstalled [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
- black==24.1.1
|
||||||
|
+ black==24.3.0
|
||||||
|
- iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
|
||||||
|
Installed: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = ["black"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue