mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-29 08:03:50 +00:00
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:
parent
3cd51ffc92
commit
31752bf4be
7 changed files with 193 additions and 53 deletions
|
@ -4,7 +4,7 @@ pub use finder::{DistFinder, Reporter as FinderReporter};
|
|||
pub use manifest::Manifest;
|
||||
pub use options::{Options, OptionsBuilder};
|
||||
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 resolver::{
|
||||
BuildId, DefaultResolverProvider, InMemoryIndex, Reporter as ResolverReporter, Resolver,
|
||||
|
|
|
@ -9,7 +9,6 @@ use petgraph::Direction;
|
|||
use pubgrub::range::Range;
|
||||
use pubgrub::solver::{Kind, State};
|
||||
use pubgrub::type_aliases::SelectedDependencies;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use url::Url;
|
||||
|
||||
|
@ -23,9 +22,20 @@ use uv_normalize::{ExtraName, PackageName};
|
|||
use crate::pins::FilePins;
|
||||
use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority};
|
||||
use crate::resolver::VersionsResponse;
|
||||
|
||||
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
|
||||
/// represents a dependency between two pinned packages.
|
||||
#[derive(Debug)]
|
||||
|
@ -256,11 +266,14 @@ pub struct DisplayResolutionGraph<'a> {
|
|||
/// Whether to include annotations in the output, to indicate which dependency or dependencies
|
||||
/// requested each package.
|
||||
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> {
|
||||
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,
|
||||
show_hashes: bool,
|
||||
include_annotations: bool,
|
||||
annotation_style: AnnotationStyle,
|
||||
) -> DisplayResolutionGraph<'a> {
|
||||
Self {
|
||||
resolution: underlying,
|
||||
show_hashes,
|
||||
include_annotations,
|
||||
annotation_style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -339,16 +354,13 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
|
|||
// Print out the dependency graph.
|
||||
for (index, node) in nodes {
|
||||
// Display the node itself.
|
||||
match node {
|
||||
Node::Distribution(_, dist) => {
|
||||
write!(f, "{}", dist.verbatim())?;
|
||||
}
|
||||
Node::Editable(_, editable) => {
|
||||
write!(f, "-e {}", editable.verbatim())?;
|
||||
}
|
||||
}
|
||||
let mut line = match node {
|
||||
Node::Distribution(_, dist) => format!("{}", dist.verbatim()),
|
||||
Node::Editable(_, editable) => format!("-e {}", editable.verbatim()),
|
||||
};
|
||||
|
||||
// Display the distribution hashes, if any.
|
||||
let mut has_hashes = false;
|
||||
if self.show_hashes {
|
||||
if let Some(hashes) = self
|
||||
.resolution
|
||||
|
@ -358,13 +370,17 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
|
|||
{
|
||||
for hash in hashes {
|
||||
if let Some(hash) = hash.to_string() {
|
||||
writeln!(f, " \\")?;
|
||||
write!(f, " --hash={hash}")?;
|
||||
has_hashes = true;
|
||||
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 {
|
||||
// Display all dependencies.
|
||||
|
@ -376,20 +392,49 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
|
|||
.collect::<Vec<_>>();
|
||||
edges.sort_unstable_by_key(|package| package.name());
|
||||
|
||||
match edges.len() {
|
||||
0 => {}
|
||||
1 => {
|
||||
for dependency in edges {
|
||||
writeln!(f, "{}", format!(" # via {}", dependency.name()).green())?;
|
||||
match self.annotation_style {
|
||||
AnnotationStyle::Line => {
|
||||
if !edges.is_empty() {
|
||||
let separator = if has_hashes { "\n " } else { " " };
|
||||
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));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
writeln!(f, "{}", " # via".green())?;
|
||||
for dependency in edges {
|
||||
writeln!(f, "{}", format!(" # {}", dependency.name()).green())?;
|
||||
AnnotationStyle::Split => match edges.as_slice() {
|
||||
[] => {}
|
||||
[edge] => {
|
||||
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}")?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,8 @@ use uv_installer::{Downloader, NoBinary};
|
|||
use uv_interpreter::{Interpreter, PythonVersion};
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_resolver::{
|
||||
DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, OptionsBuilder,
|
||||
PreReleaseMode, ResolutionMode, Resolver,
|
||||
AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
|
||||
OptionsBuilder, PreReleaseMode, ResolutionMode, Resolver,
|
||||
};
|
||||
use uv_traits::{InFlight, NoBuild, SetupPyStrategy};
|
||||
use uv_warnings::warn_user;
|
||||
|
@ -62,6 +62,7 @@ pub(crate) async fn pip_compile(
|
|||
no_build: &NoBuild,
|
||||
python_version: Option<PythonVersion>,
|
||||
exclude_newer: Option<DateTime<Utc>>,
|
||||
annotation_style: AnnotationStyle,
|
||||
cache: Cache,
|
||||
mut printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
@ -391,7 +392,12 @@ pub(crate) async fn pip_compile(
|
|||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
DisplayResolutionGraph::new(&resolution, generate_hashes, include_annotations)
|
||||
DisplayResolutionGraph::new(
|
||||
&resolution,
|
||||
generate_hashes,
|
||||
include_annotations,
|
||||
annotation_style
|
||||
)
|
||||
)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
|
|
|
@ -36,9 +36,6 @@ pub(crate) struct PipCompileCompatArgs {
|
|||
#[clap(long, hide = true)]
|
||||
resolver: Option<Resolver>,
|
||||
|
||||
#[clap(long, hide = true)]
|
||||
annotation_style: Option<AnnotationStyle>,
|
||||
|
||||
#[clap(long, hide = true)]
|
||||
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() {
|
||||
return Err(anyhow!(
|
||||
"pip-compile's `--max-rounds` is unsupported (uv always resolves until convergence)."
|
||||
|
@ -346,12 +328,6 @@ enum Resolver {
|
|||
Legacy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, ValueEnum)]
|
||||
enum AnnotationStyle {
|
||||
Line,
|
||||
Split,
|
||||
}
|
||||
|
||||
/// Arguments for `venv` compatibility.
|
||||
///
|
||||
/// These represent a subset of the `virtualenv` interface that uv supports by default.
|
||||
|
|
|
@ -19,7 +19,7 @@ use uv_client::Connectivity;
|
|||
use uv_installer::{NoBinary, Reinstall};
|
||||
use uv_interpreter::PythonVersion;
|
||||
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 crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade};
|
||||
|
@ -354,6 +354,10 @@ struct PipCompileArgs {
|
|||
#[clap(long, hide = true)]
|
||||
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)]
|
||||
compat_args: compat::PipCompileCompatArgs,
|
||||
}
|
||||
|
@ -898,6 +902,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
&no_build,
|
||||
args.python_version,
|
||||
args.exclude_newer,
|
||||
args.annotation_style,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
let windows_only_deps = [
|
||||
("( [+-] )?colorama==\\d+(\\.[\\d+])+\n( # via .*\n)?"),
|
||||
("( [+-] )?colorama==\\d+(\\.[\\d+])+\\s+(# via .*\n)?"),
|
||||
("( [+-] )?tzdata==\\d+(\\.[\\d+])+\n( # via .*\n)?"),
|
||||
("( [+-] )?tzdata==\\d+(\\.[\\d+])+\\s+(# via .*\n)?"),
|
||||
];
|
||||
let mut removed_packages = 0;
|
||||
for windows_only_dep in windows_only_deps {
|
||||
|
|
|
@ -47,6 +47,33 @@ fn compile_requirements_in() -> Result<()> {
|
|||
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
|
||||
/// when passed a path of `-`.
|
||||
#[test]
|
||||
|
@ -550,6 +577,38 @@ fn compile_python_312() -> Result<()> {
|
|||
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.
|
||||
#[test]
|
||||
fn compile_python_312_no_deps() -> Result<()> {
|
||||
|
@ -1426,6 +1485,53 @@ optional-dependencies.bar = [
|
|||
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.
|
||||
#[test]
|
||||
fn compile_does_not_allow_both_extra_and_all_extras() -> Result<()> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue