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:
Charlie Marsh 2024-07-02 16:46:31 -04:00 committed by GitHub
parent 21187e1f36
commit 32dc9bef59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 293 additions and 62 deletions

1
Cargo.lock generated
View file

@ -5039,6 +5039,7 @@ dependencies = [
"pathdiff", "pathdiff",
"pep440_rs", "pep440_rs",
"pep508_rs", "pep508_rs",
"pypi-types",
"serde", "serde",
"thiserror", "thiserror",
"toml", "toml",

View file

@ -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 {

View file

@ -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 }

View file

@ -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 {

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -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" },
]
"###);
});
}