Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --no-emit-project and friends to uv export #7110

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2970,6 +2970,29 @@ pub struct ExportArgs {
#[arg(long, short)]
pub output_file: Option<PathBuf>,

/// 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, alias = "no-install-project")]
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, alias = "no-install-workspace")]
pub no_emit_workspace: bool,

/// Do not emit 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, alias = "no-install-package")]
pub no_emit_package: Vec<PackageName>,

/// Assert that the `uv.lock` will remain unchanged.
///
/// Requires that the lockfile is up-to-date. If the lockfile is missing or
Expand Down
25 changes: 25 additions & 0 deletions crates/uv-configuration/src/install_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PackageName>,
) -> 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
}
}
50 changes: 30 additions & 20 deletions crates/uv-resolver/src/lock/requirements_txt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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};
Expand All @@ -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<NodeIndex, MarkerTree>,
nodes: Vec<Node<'lock>>,
hashes: bool,
}

Expand All @@ -39,6 +43,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
extras: &ExtrasSpecification,
dev: &[GroupName],
hashes: bool,
install_options: &'lock InstallOptions,
) -> Result<Self, LockError> {
let size_guess = lock.packages.len();
let mut petgraph = LockGraph::with_capacity(size_guess, size_guess);
Expand Down Expand Up @@ -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::<Vec<_>>();
let mut nodes: Vec<Node> = 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::<Vec<_>>();

// 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)?;
Expand Down Expand Up @@ -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}")?;
}

Expand Down
4 changes: 3 additions & 1 deletion crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -24,6 +24,7 @@ pub(crate) async fn export(
format: ExportFormat,
package: Option<PackageName>,
hashes: bool,
install_options: InstallOptions,
output_file: Option<PathBuf>,
extras: ExtrasSpecification,
dev: bool,
Expand Down Expand Up @@ -125,6 +126,7 @@ pub(crate) async fn export(
&extras,
&dev,
hashes,
&install_options,
)?;
writeln!(
writer,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,7 @@ async fn run_project(
args.format,
args.package,
args.hashes,
args.install_options,
args.output_file,
args.extras,
args.dev,
Expand Down
9 changes: 9 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
pub(crate) locked: bool,
pub(crate) frozen: bool,
Expand All @@ -980,6 +981,9 @@ impl ExportSettings {
hashes,
no_hashes,
output_file,
no_emit_project,
no_emit_workspace,
no_emit_package,
locked,
frozen,
resolver,
Expand All @@ -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,
Expand Down
128 changes: 128 additions & 0 deletions crates/uv/tests/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
12 changes: 12 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1857,6 +1857,18 @@ uv export [OPTIONS]
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p>
</dd><dt><code>--no-dev</code></dt><dd><p>Omit development dependencies</p>

</dd><dt><code>--no-emit-package</code> <i>no-emit-package</i></dt><dd><p>Do not emit the given package(s).</p>

<p>By default, all of the project&#8217;s dependencies are included in the exported requirements file. The <code>--no-install-package</code> option allows exclusion of specific packages.</p>

</dd><dt><code>--no-emit-project</code></dt><dd><p>Do not emit the current project.</p>

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

</dd><dt><code>--no-emit-workspace</code></dt><dd><p>Do not emit any workspace members, including the root project.</p>

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

</dd><dt><code>--no-hashes</code></dt><dd><p>Omit hashes in the generated output</p>

</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
Expand Down
Loading