From df7fce92785695c3d57bf62d31325bb649b8a2c0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Sep 2024 20:43:11 -0400 Subject: [PATCH] Add --no-emit-project and friends to uv export --- crates/uv-cli/src/lib.rs | 23 ++++ .../uv-configuration/src/install_options.rs | 25 ++++ .../uv-resolver/src/lock/requirements_txt.rs | 50 ++++--- crates/uv/src/commands/project/export.rs | 4 +- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 9 ++ crates/uv/tests/export.rs | 128 ++++++++++++++++++ docs/reference/cli.md | 12 ++ 8 files changed, 231 insertions(+), 21 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index a022144ff729e..c8520de48b3f2 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2970,6 +2970,29 @@ pub struct ExportArgs { #[arg(long, short)] pub output_file: Option, + /// Do not emit the current project. + /// + /// By default, the current project is included in the exported requirements file with all of its + /// dependencies. The `--no-emit-project` option allows the project to be excluded, but all of + /// its dependencies to remain included. + #[arg(long)] + pub no_emit_project: bool, + + /// Do not emit any workspace members, including the root project. + /// + /// By default, all workspace members and their dependencies are included in the exported + /// requirements file, with all of their dependencies. The `--no-emit-workspace` option allows + /// exclusion of all the workspace members while retaining their dependencies. + #[arg(long)] + pub no_emit_workspace: bool, + + /// Do not install the given package(s). + /// + /// By default, all of the project's dependencies are included in the exported requirements + /// file. The `--no-install-package` option allows exclusion of specific packages. + #[arg(long)] + pub no_emit_package: Vec, + /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or diff --git a/crates/uv-configuration/src/install_options.rs b/crates/uv-configuration/src/install_options.rs index 141867f9b9f34..5be621b2d3a37 100644 --- a/crates/uv-configuration/src/install_options.rs +++ b/crates/uv-configuration/src/install_options.rs @@ -80,4 +80,29 @@ impl InstallOptions { resolution.filter(|dist| !no_install_packages.contains(dist.name())) } + + /// Returns `true` if a package passes the install filters. + pub fn include_package( + &self, + package: &PackageName, + project_name: &PackageName, + members: &BTreeSet, + ) -> bool { + // If `--no-install-project` is set, remove the project itself. + if self.no_install_project && package == project_name { + return false; + } + + // If `--no-install-workspace` is set, remove the project and any workspace members. + if self.no_install_workspace && members.contains(package) { + return false; + } + + // If `--no-install-package` is provided, remove the requested packages. + if self.no_install_package.contains(package) { + return false; + } + + true + } } diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index 516cc44703931..dcad09ee49513 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -4,7 +4,6 @@ use std::fmt::Formatter; use std::path::{Path, PathBuf}; use either::Either; -use petgraph::graph::NodeIndex; use petgraph::visit::IntoNodeReferences; use petgraph::{Directed, Graph}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -13,7 +12,7 @@ use url::Url; use distribution_filename::{DistExtension, SourceDistExtension}; use pep508_rs::MarkerTree; use pypi_types::{ParsedArchiveUrl, ParsedGitUrl}; -use uv_configuration::ExtrasSpecification; +use uv_configuration::{ExtrasSpecification, InstallOptions}; use uv_fs::Simplified; use uv_git::GitReference; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -24,11 +23,16 @@ use crate::{Lock, LockError}; type LockGraph<'lock> = Graph<&'lock Package, Edge, Directed>; +#[derive(Debug, Clone, PartialEq, Eq)] +struct Node<'lock> { + package: &'lock Package, + marker: MarkerTree, +} + /// An export of a [`Lock`] that renders in `requirements.txt` format. #[derive(Debug)] pub struct RequirementsTxtExport<'lock> { - graph: LockGraph<'lock>, - reachability: FxHashMap, + nodes: Vec>, hashes: bool, } @@ -39,6 +43,7 @@ impl<'lock> RequirementsTxtExport<'lock> { extras: &ExtrasSpecification, dev: &[GroupName], hashes: bool, + install_options: &'lock InstallOptions, ) -> Result { let size_guess = lock.packages.len(); let mut petgraph = LockGraph::with_capacity(size_guess, size_guess); @@ -123,28 +128,33 @@ impl<'lock> RequirementsTxtExport<'lock> { } } - let reachability = marker_reachability(&petgraph, &[]); - - Ok(Self { - graph: petgraph, - reachability, - hashes, - }) - } -} + let mut reachability = marker_reachability(&petgraph, &[]); -impl std::fmt::Display for RequirementsTxtExport<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { // Collect all packages. - let mut nodes = self.graph.node_references().collect::>(); + let mut nodes: Vec = petgraph + .node_references() + .filter(|(_index, package)| { + install_options.include_package(&package.id.name, root_name, lock.members()) + }) + .map(|(index, package)| Node { + package, + marker: reachability.remove(&index).unwrap_or_default(), + }) + .collect::>(); // Sort the nodes, such that unnamed URLs (editables) appear at the top. - nodes.sort_unstable_by(|(_, a), (_, b)| { - NodeComparator::from(**a).cmp(&NodeComparator::from(**b)) + nodes.sort_unstable_by(|a, b| { + NodeComparator::from(a.package).cmp(&NodeComparator::from(b.package)) }); + Ok(Self { nodes, hashes }) + } +} + +impl std::fmt::Display for RequirementsTxtExport<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { // Write out each package. - for (node_index, package) in nodes { + for Node { package, marker } in &self.nodes { match &package.id.source { Source::Registry(_) => { write!(f, "{}=={}", package.id.name, package.id.version)?; @@ -201,7 +211,7 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { } } - if let Some(contents) = self.reachability[&node_index].contents() { + if let Some(contents) = marker.contents() { write!(f, " ; {contents}")?; } diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 2d274d335f2e1..b3a58573232e0 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use uv_cache::Cache; use uv_client::Connectivity; -use uv_configuration::{Concurrency, ExportFormat, ExtrasSpecification}; +use uv_configuration::{Concurrency, ExportFormat, ExtrasSpecification, InstallOptions}; use uv_fs::CWD; use uv_normalize::{PackageName, DEV_DEPENDENCIES}; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; @@ -24,6 +24,7 @@ pub(crate) async fn export( format: ExportFormat, package: Option, hashes: bool, + install_options: InstallOptions, output_file: Option, extras: ExtrasSpecification, dev: bool, @@ -125,6 +126,7 @@ pub(crate) async fn export( &extras, &dev, hashes, + &install_options, )?; writeln!( writer, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f645305293b28..df4780c0a9bad 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1323,6 +1323,7 @@ async fn run_project( args.format, args.package, args.hashes, + args.install_options, args.output_file, args.extras, args.dev, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 410040d9f06fe..1aae167b540c0 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -957,6 +957,7 @@ pub(crate) struct ExportSettings { pub(crate) extras: ExtrasSpecification, pub(crate) dev: bool, pub(crate) hashes: bool, + pub(crate) install_options: InstallOptions, pub(crate) output_file: Option, pub(crate) locked: bool, pub(crate) frozen: bool, @@ -980,6 +981,9 @@ impl ExportSettings { hashes, no_hashes, output_file, + no_emit_project, + no_emit_workspace, + no_emit_package, locked, frozen, resolver, @@ -997,6 +1001,11 @@ impl ExportSettings { ), dev: flag(dev, no_dev).unwrap_or(true), hashes: flag(hashes, no_hashes).unwrap_or(true), + install_options: InstallOptions::new( + no_emit_project, + no_emit_workspace, + no_emit_package, + ), output_file, locked, frozen, diff --git a/crates/uv/tests/export.rs b/crates/uv/tests/export.rs index 64b78fd37adf4..20d1f98226a83 100644 --- a/crates/uv/tests/export.rs +++ b/crates/uv/tests/export.rs @@ -695,3 +695,131 @@ fn output_file() -> Result<()> { Ok(()) } + +#[test] +fn no_emit() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0", "child"] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv.sources] + child = { workspace = true } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child = context.temp_dir.child("child"); + child.child("pyproject.toml").write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>=2"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + // Exclude `anyio`. + uv_snapshot!(context.filters(), context.export().arg("--no-emit-package").arg("anyio"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated via `uv export`. + -e . + -e child + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + sniffio==1.3.1 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + // Exclude `project`. + uv_snapshot!(context.filters(), context.export().arg("--no-emit-project"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated via `uv export`. + -e child + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + sniffio==1.3.1 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + // Exclude `child`. + uv_snapshot!(context.filters(), context.export().arg("--no-emit-project").arg("--package").arg("child"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated via `uv export`. + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + // Exclude the workspace. + uv_snapshot!(context.filters(), context.export().arg("--no-emit-workspace"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated via `uv export`. + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + sniffio==1.3.1 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + Ok(()) +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index c653e41833b43..dfe77431af7b0 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1857,6 +1857,18 @@ uv export [OPTIONS]

May also be set with the UV_NO_CONFIG environment variable.

--no-dev

Omit development dependencies

+
--no-emit-package no-emit-package

Do not install the given package(s).

+ +

By default, all of the project’s dependencies are included in the exported requirements file. The --no-install-package option allows exclusion of specific packages.

+ +
--no-emit-project

Do not emit the current project.

+ +

By default, the current project is included in the exported requirements file with all of its dependencies. The --no-emit-project option allows the project to be excluded, but all of its dependencies to remain included.

+ +
--no-emit-workspace

Do not emit any workspace members, including the root project.

+ +

By default, all workspace members and their dependencies are included in the exported requirements file, with all of their dependencies. The --no-emit-workspace option allows exclusion of all the workspace members while retaining their dependencies.

+
--no-hashes

Omit hashes in the generated output

--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links