Add support for pip-compile's `--unsafe-package` flag (#1889)

## Summary

In uv, we're going to use `--no-emit-package` for this, to convey that
the package will be included in the resolution but not in the output
file. It also mirrors flags like `--emit-index-url`.

We're also including an `--unsafe-package` alias.

Closes https://github.com/astral-sh/uv/issues/1415.
This commit is contained in:
Charlie Marsh 2024-02-23 13:47:36 -05:00 committed by GitHub
parent 9cf7d113bc
commit eaf613ed31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 86 additions and 12 deletions

View File

@ -257,6 +257,13 @@ impl ResolutionGraph {
self.petgraph.node_count() == 0 self.petgraph.node_count() == 0
} }
/// Returns `true` if the graph contains the given package.
pub fn contains(&self, name: &PackageName) -> bool {
self.petgraph
.node_indices()
.any(|index| self.petgraph[index].name() == name)
}
/// Return the [`Diagnostic`]s that were encountered while building the graph. /// Return the [`Diagnostic`]s that were encountered while building the graph.
pub fn diagnostics(&self) -> &[Diagnostic] { pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics &self.diagnostics
@ -273,6 +280,8 @@ impl ResolutionGraph {
pub struct DisplayResolutionGraph<'a> { pub struct DisplayResolutionGraph<'a> {
/// The underlying graph. /// The underlying graph.
resolution: &'a ResolutionGraph, resolution: &'a ResolutionGraph,
/// The packages to exclude from the output.
no_emit_packages: &'a [PackageName],
/// Whether to include hashes in the output. /// Whether to include hashes in the output.
show_hashes: bool, show_hashes: bool,
/// 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
@ -285,7 +294,7 @@ pub struct DisplayResolutionGraph<'a> {
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, AnnotationStyle::default()) Self::new(resolution, &[], false, true, AnnotationStyle::default())
} }
} }
@ -293,12 +302,14 @@ impl<'a> DisplayResolutionGraph<'a> {
/// Create a new [`DisplayResolutionGraph`] for the given graph. /// Create a new [`DisplayResolutionGraph`] for the given graph.
pub fn new( pub fn new(
underlying: &'a ResolutionGraph, underlying: &'a ResolutionGraph,
no_emit_packages: &'a [PackageName],
show_hashes: bool, show_hashes: bool,
include_annotations: bool, include_annotations: bool,
annotation_style: AnnotationStyle, annotation_style: AnnotationStyle,
) -> DisplayResolutionGraph<'a> { ) -> DisplayResolutionGraph<'a> {
Self { Self {
resolution: underlying, resolution: underlying,
no_emit_packages,
show_hashes, show_hashes,
include_annotations, include_annotations,
annotation_style, annotation_style,
@ -348,15 +359,19 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
.resolution .resolution
.petgraph .petgraph
.node_indices() .node_indices()
.map(|index| { .filter_map(|index| {
let dist = &self.resolution.petgraph[index]; let dist = &self.resolution.petgraph[index];
let name = dist.name(); let name = dist.name();
if self.no_emit_packages.contains(name) {
return None;
}
let node = if let Some((editable, _)) = self.resolution.editables.get(name) { let node = if let Some((editable, _)) = self.resolution.editables.get(name) {
Node::Editable(name, editable) Node::Editable(name, editable)
} else { } else {
Node::Distribution(name, dist) Node::Distribution(name, dist)
}; };
(index, node) Some((index, node))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@ -52,6 +52,7 @@ pub(crate) async fn pip_compile(
dependency_mode: DependencyMode, dependency_mode: DependencyMode,
upgrade: Upgrade, upgrade: Upgrade,
generate_hashes: bool, generate_hashes: bool,
no_emit_packages: Vec<PackageName>,
include_annotations: bool, include_annotations: bool,
include_header: bool, include_header: bool,
include_index_url: bool, include_index_url: bool,
@ -383,12 +384,30 @@ pub(crate) async fn pip_compile(
"{}", "{}",
DisplayResolutionGraph::new( DisplayResolutionGraph::new(
&resolution, &resolution,
&no_emit_packages,
generate_hashes, generate_hashes,
include_annotations, include_annotations,
annotation_style, annotation_style,
) )
)?; )?;
// If any "unsafe" packages were excluded, notify the user.
let excluded = no_emit_packages
.into_iter()
.filter(|name| resolution.contains(name))
.collect::<Vec<_>>();
if !excluded.is_empty() {
writeln!(writer)?;
writeln!(
writer,
"{}",
"# The following packages were included while generating the resolution:".green()
)?;
for package in excluded {
writeln!(writer, "# {package}")?;
}
}
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }

View File

@ -54,9 +54,6 @@ pub(crate) struct PipCompileCompatArgs {
#[clap(long, hide = true)] #[clap(long, hide = true)]
no_emit_trusted_host: bool, no_emit_trusted_host: bool,
#[clap(long, hide = true)]
unsafe_package: Vec<String>,
#[clap(long, hide = true)] #[clap(long, hide = true)]
config: Option<String>, config: Option<String>,
@ -171,12 +168,6 @@ impl CompatArgs for PipCompileCompatArgs {
); );
} }
if !self.unsafe_package.is_empty() {
return Err(anyhow!(
"pip-compile's `--unsafe-package` is not supported."
));
}
if self.config.is_some() { if self.config.is_some() {
return Err(anyhow!( return Err(anyhow!(
"pip-compile's `--config` is unsupported (uv does not use a configuration file)." "pip-compile's `--config` is unsupported (uv does not use a configuration file)."

View File

@ -352,6 +352,11 @@ struct PipCompileArgs {
#[arg(long, value_parser = date_or_datetime, hide = true)] #[arg(long, value_parser = date_or_datetime, hide = true)]
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,
/// Specify a package to omit from the output resolution. Its dependencies will still be
/// included in the resolution. Equivalent to pip-compile's `--unsafe-package` option.
#[clap(long, alias = "unsafe-package")]
no_emit_package: Vec<PackageName>,
/// Include `--index-url` and `--extra-index-url` entries in the generated output file. /// Include `--index-url` and `--extra-index-url` entries in the generated output file.
#[clap(long, hide = true)] #[clap(long, hide = true)]
emit_index_url: bool, emit_index_url: bool,
@ -904,6 +909,7 @@ async fn run() -> Result<ExitStatus> {
dependency_mode, dependency_mode,
upgrade, upgrade,
args.generate_hashes, args.generate_hashes,
args.no_emit_package,
!args.no_annotate, !args.no_annotate,
!args.no_header, !args.no_header,
args.emit_index_url, args.emit_index_url,

View File

@ -4225,3 +4225,46 @@ fn override_with_incompatible_constraint() -> Result<()> {
Ok(()) Ok(())
} }
/// Resolve a package, marking a dependency as unsafe.
#[test]
fn unsafe_package() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("flask")?;
uv_snapshot!(context.compile()
.arg("requirements.in")
.arg("--unsafe-package")
.arg("jinja2")
.arg("--unsafe-package")
.arg("pydantic"), @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 requirements.in --unsafe-package jinja2 --unsafe-package pydantic
blinker==1.7.0
# via flask
click==8.1.7
# via flask
flask==3.0.0
itsdangerous==2.1.2
# via flask
markupsafe==2.1.3
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
# The following packages were included while generating the resolution:
# jinja2
----- stderr -----
Resolved 7 packages in [TIME]
"###
);
Ok(())
}