feat: Implement --annotation-style parameter for uv pip compile (#1679)

## Summary

Hello there! The motivation for this feature is described here #1678 

## Test Plan

I've added unit tests and also tested this manually on my work project
by comparing it to the original `pip-compile` output - it looks much
like the `pip-compile` generated lock file.
This commit is contained in:
Evgeniy Dubovskoy 2024-02-21 04:08:34 +02:00 committed by GitHub
parent 3cd51ffc92
commit 31752bf4be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 193 additions and 53 deletions

View file

@ -4,7 +4,7 @@ pub use finder::{DistFinder, Reporter as FinderReporter};
pub use manifest::Manifest; pub use manifest::Manifest;
pub use options::{Options, OptionsBuilder}; pub use options::{Options, OptionsBuilder};
pub use prerelease_mode::PreReleaseMode; pub use prerelease_mode::PreReleaseMode;
pub use resolution::{Diagnostic, DisplayResolutionGraph, ResolutionGraph}; pub use resolution::{AnnotationStyle, Diagnostic, DisplayResolutionGraph, ResolutionGraph};
pub use resolution_mode::ResolutionMode; pub use resolution_mode::ResolutionMode;
pub use resolver::{ pub use resolver::{
BuildId, DefaultResolverProvider, InMemoryIndex, Reporter as ResolverReporter, Resolver, BuildId, DefaultResolverProvider, InMemoryIndex, Reporter as ResolverReporter, Resolver,

View file

@ -9,7 +9,6 @@ use petgraph::Direction;
use pubgrub::range::Range; use pubgrub::range::Range;
use pubgrub::solver::{Kind, State}; use pubgrub::solver::{Kind, State};
use pubgrub::type_aliases::SelectedDependencies; use pubgrub::type_aliases::SelectedDependencies;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use url::Url; use url::Url;
@ -23,9 +22,20 @@ use uv_normalize::{ExtraName, PackageName};
use crate::pins::FilePins; use crate::pins::FilePins;
use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority}; use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority};
use crate::resolver::VersionsResponse; use crate::resolver::VersionsResponse;
use crate::ResolveError; use crate::ResolveError;
/// Indicate the style of annotation comments, used to indicate the dependencies that requested each
/// package.
#[derive(Debug, Default, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum AnnotationStyle {
/// Render the annotations on a single, comma-separated line.
Line,
/// Render each annotation on its own line.
#[default]
Split,
}
/// A complete resolution graph in which every node represents a pinned package and every edge /// A complete resolution graph in which every node represents a pinned package and every edge
/// represents a dependency between two pinned packages. /// represents a dependency between two pinned packages.
#[derive(Debug)] #[derive(Debug)]
@ -256,11 +266,14 @@ pub struct DisplayResolutionGraph<'a> {
/// Whether to include annotations in the output, to indicate which dependency or dependencies /// Whether to include annotations in the output, to indicate which dependency or dependencies
/// requested each package. /// requested each package.
include_annotations: bool, include_annotations: bool,
/// The style of annotation comments, used to indicate the dependencies that requested each
/// package.
annotation_style: AnnotationStyle,
} }
impl<'a> From<&'a ResolutionGraph> for DisplayResolutionGraph<'a> { impl<'a> From<&'a ResolutionGraph> for DisplayResolutionGraph<'a> {
fn from(resolution: &'a ResolutionGraph) -> Self { fn from(resolution: &'a ResolutionGraph) -> Self {
Self::new(resolution, false, true) Self::new(resolution, false, true, AnnotationStyle::default())
} }
} }
@ -270,11 +283,13 @@ impl<'a> DisplayResolutionGraph<'a> {
underlying: &'a ResolutionGraph, underlying: &'a ResolutionGraph,
show_hashes: bool, show_hashes: bool,
include_annotations: bool, include_annotations: bool,
annotation_style: AnnotationStyle,
) -> DisplayResolutionGraph<'a> { ) -> DisplayResolutionGraph<'a> {
Self { Self {
resolution: underlying, resolution: underlying,
show_hashes, show_hashes,
include_annotations, include_annotations,
annotation_style,
} }
} }
} }
@ -339,16 +354,13 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
// Print out the dependency graph. // Print out the dependency graph.
for (index, node) in nodes { for (index, node) in nodes {
// Display the node itself. // Display the node itself.
match node { let mut line = match node {
Node::Distribution(_, dist) => { Node::Distribution(_, dist) => format!("{}", dist.verbatim()),
write!(f, "{}", dist.verbatim())?; Node::Editable(_, editable) => format!("-e {}", editable.verbatim()),
} };
Node::Editable(_, editable) => {
write!(f, "-e {}", editable.verbatim())?;
}
}
// Display the distribution hashes, if any. // Display the distribution hashes, if any.
let mut has_hashes = false;
if self.show_hashes { if self.show_hashes {
if let Some(hashes) = self if let Some(hashes) = self
.resolution .resolution
@ -358,13 +370,17 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
{ {
for hash in hashes { for hash in hashes {
if let Some(hash) = hash.to_string() { if let Some(hash) = hash.to_string() {
writeln!(f, " \\")?; has_hashes = true;
write!(f, " --hash={hash}")?; line.push_str(" \\\n");
line.push_str(" --hash=");
line.push_str(&hash);
} }
} }
} }
} }
writeln!(f)?;
// Determine the annotation comment and separator (between comment and requirement).
let mut annotation = None;
if self.include_annotations { if self.include_annotations {
// Display all dependencies. // Display all dependencies.
@ -376,21 +392,50 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
edges.sort_unstable_by_key(|package| package.name()); edges.sort_unstable_by_key(|package| package.name());
match edges.len() { match self.annotation_style {
0 => {} AnnotationStyle::Line => {
1 => { if !edges.is_empty() {
for dependency in edges { let separator = if has_hashes { "\n " } else { " " };
writeln!(f, "{}", format!(" # via {}", dependency.name()).green())?; let deps = edges
.into_iter()
.map(|dependency| dependency.name().to_string())
.collect::<Vec<_>>()
.join(", ");
let comment = format!("# via {deps}").green().to_string();
annotation = Some((separator, comment));
} }
} }
_ => { AnnotationStyle::Split => match edges.as_slice() {
writeln!(f, "{}", " # via".green())?; [] => {}
for dependency in edges { [edge] => {
writeln!(f, "{}", format!(" # {}", dependency.name()).green())?; let separator = "\n";
let comment = format!(" # via {}", edge.name()).green().to_string();
annotation = Some((separator, comment));
} }
} edges => {
let separator = "\n";
let deps = edges
.iter()
.map(|dependency| format!(" # {}", dependency.name()))
.collect::<Vec<_>>()
.join("\n");
let comment = format!(" # via\n{deps}").green().to_string();
annotation = Some((separator, comment));
}
},
} }
} }
if let Some((separator, comment)) = annotation {
// Assemble the line with the annotations and remove trailing whitespaces.
for line in format!("{line:24}{separator}{comment}").lines() {
let line = line.trim_end();
writeln!(f, "{line}")?;
}
} else {
// Write the line as is.
writeln!(f, "{line}")?;
}
} }
Ok(()) Ok(())

View file

@ -28,8 +28,8 @@ use uv_installer::{Downloader, NoBinary};
use uv_interpreter::{Interpreter, PythonVersion}; use uv_interpreter::{Interpreter, PythonVersion};
use uv_normalize::{ExtraName, PackageName}; use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{ use uv_resolver::{
DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, OptionsBuilder, AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
PreReleaseMode, ResolutionMode, Resolver, OptionsBuilder, PreReleaseMode, ResolutionMode, Resolver,
}; };
use uv_traits::{InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{InFlight, NoBuild, SetupPyStrategy};
use uv_warnings::warn_user; use uv_warnings::warn_user;
@ -62,6 +62,7 @@ pub(crate) async fn pip_compile(
no_build: &NoBuild, no_build: &NoBuild,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,
annotation_style: AnnotationStyle,
cache: Cache, cache: Cache,
mut printer: Printer, mut printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
@ -391,7 +392,12 @@ pub(crate) async fn pip_compile(
write!( write!(
writer, writer,
"{}", "{}",
DisplayResolutionGraph::new(&resolution, generate_hashes, include_annotations) DisplayResolutionGraph::new(
&resolution,
generate_hashes,
include_annotations,
annotation_style
)
)?; )?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)

View file

@ -36,9 +36,6 @@ pub(crate) struct PipCompileCompatArgs {
#[clap(long, hide = true)] #[clap(long, hide = true)]
resolver: Option<Resolver>, resolver: Option<Resolver>,
#[clap(long, hide = true)]
annotation_style: Option<AnnotationStyle>,
#[clap(long, hide = true)] #[clap(long, hide = true)]
max_rounds: Option<usize>, max_rounds: Option<usize>,
@ -144,21 +141,6 @@ impl CompatArgs for PipCompileCompatArgs {
} }
} }
if let Some(annotation_style) = self.annotation_style {
match annotation_style {
AnnotationStyle::Split => {
warn_user!(
"pip-compile's `--annotation-style=split` has no effect (uv always emits split annotations)."
);
}
AnnotationStyle::Line => {
return Err(anyhow!(
"pip-compile's `--annotation-style=line` is unsupported (uv always emits split annotations)."
));
}
}
}
if self.max_rounds.is_some() { if self.max_rounds.is_some() {
return Err(anyhow!( return Err(anyhow!(
"pip-compile's `--max-rounds` is unsupported (uv always resolves until convergence)." "pip-compile's `--max-rounds` is unsupported (uv always resolves until convergence)."
@ -346,12 +328,6 @@ enum Resolver {
Legacy, Legacy,
} }
#[derive(Debug, Copy, Clone, ValueEnum)]
enum AnnotationStyle {
Line,
Split,
}
/// Arguments for `venv` compatibility. /// Arguments for `venv` compatibility.
/// ///
/// These represent a subset of the `virtualenv` interface that uv supports by default. /// These represent a subset of the `virtualenv` interface that uv supports by default.

View file

@ -19,7 +19,7 @@ use uv_client::Connectivity;
use uv_installer::{NoBinary, Reinstall}; use uv_installer::{NoBinary, Reinstall};
use uv_interpreter::PythonVersion; use uv_interpreter::PythonVersion;
use uv_normalize::{ExtraName, PackageName}; use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{DependencyMode, PreReleaseMode, ResolutionMode}; use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode};
use uv_traits::{NoBuild, PackageNameSpecifier, SetupPyStrategy}; use uv_traits::{NoBuild, PackageNameSpecifier, SetupPyStrategy};
use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade}; use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade};
@ -354,6 +354,10 @@ struct PipCompileArgs {
#[clap(long, hide = true)] #[clap(long, hide = true)]
emit_find_links: bool, emit_find_links: bool,
/// Choose the style of the annotation comments, which indicate the source of each package.
#[clap(long, default_value_t=AnnotationStyle::Split, value_enum)]
annotation_style: AnnotationStyle,
#[command(flatten)] #[command(flatten)]
compat_args: compat::PipCompileCompatArgs, compat_args: compat::PipCompileCompatArgs,
} }
@ -898,6 +902,7 @@ async fn run() -> Result<ExitStatus> {
&no_build, &no_build,
args.python_version, args.python_version,
args.exclude_newer, args.exclude_newer,
args.annotation_style,
cache, cache,
printer, printer,
) )

View file

@ -266,7 +266,9 @@ pub fn run_and_format<'a>(
// 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)?"),
("( [+-] )?tzdata==\\d+(\\.[\\d+])+\n( # via .*\n)?"), ("( [+-] )?tzdata==\\d+(\\.[\\d+])+\n( # 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

@ -47,6 +47,33 @@ fn compile_requirements_in() -> Result<()> {
Ok(()) Ok(())
} }
/// Resolve a specific version of Django from a `requirements.in` file with a `--annotation-style=line` flag.
#[test]
fn compile_requirements_in_annotation_line() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("django==5.0b1")?;
uv_snapshot!(context
.compile()
.arg("--annotation-style=line")
.arg("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z --annotation-style=line requirements.in
asgiref==3.7.2 # via django
django==5.0b1
sqlparse==0.4.4 # via django
----- stderr -----
Resolved 3 packages in [TIME]
"###);
Ok(())
}
/// Resolve a specific version of Django from a `requirements.in` file on stdin /// Resolve a specific version of Django from a `requirements.in` file on stdin
/// when passed a path of `-`. /// when passed a path of `-`.
#[test] #[test]
@ -550,6 +577,38 @@ fn compile_python_312() -> Result<()> {
Ok(()) Ok(())
} }
/// Resolve a specific version of Black at Python 3.12 with `--annotation-style=line`.
#[test]
fn compile_python_312_annotation_line() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("black==23.10.1")?;
uv_snapshot!(context.compile()
.arg("--annotation-style=line")
.arg("requirements.in")
.arg("--python-version")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z --annotation-style=line requirements.in --python-version 3.12
black==23.10.1
click==8.1.7 # via black
mypy-extensions==1.0.0 # via black
packaging==23.2 # via black
pathspec==0.11.2 # via black
platformdirs==4.0.0 # via black
----- stderr -----
Resolved 6 packages in [TIME]
"###
);
Ok(())
}
/// Resolve a specific version of Black at Python 3.12 without deps. /// Resolve a specific version of Black at Python 3.12 without deps.
#[test] #[test]
fn compile_python_312_no_deps() -> Result<()> { fn compile_python_312_no_deps() -> Result<()> {
@ -1426,6 +1485,53 @@ optional-dependencies.bar = [
Ok(()) Ok(())
} }
#[test]
fn compile_pyproject_toml_all_extras_annotation_line() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[build-system]
requires = ["setuptools", "wheel"]
[project]
name = "project"
dependencies = ["django==5.0b1"]
optional-dependencies.foo = [
"anyio==4.0.0",
]
optional-dependencies.bar = [
"httpcore==0.18.0",
]
"#,
)?;
uv_snapshot!(context.compile()
.arg("--annotation-style=line")
.arg("pyproject.toml")
.arg("--all-extras"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z --annotation-style=line pyproject.toml --all-extras
anyio==4.0.0 # via httpcore
asgiref==3.7.2 # via django
certifi==2023.11.17 # via httpcore
django==5.0b1
h11==0.14.0 # via httpcore
httpcore==0.18.0
idna==3.4 # via anyio
sniffio==1.3.0 # via anyio, httpcore
sqlparse==0.4.4 # via django
----- stderr -----
Resolved 9 packages in [TIME]
"###
);
Ok(())
}
/// Resolve packages from all optional dependency groups in a `pyproject.toml` file. /// Resolve packages from all optional dependency groups in a `pyproject.toml` file.
#[test] #[test]
fn compile_does_not_allow_both_extra_and_all_extras() -> Result<()> { fn compile_does_not_allow_both_extra_and_all_extras() -> Result<()> {