Skip to content

Commit

Permalink
Add --no-emit-project and friends to uv export
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Sep 6, 2024
1 parent 8bdba6c commit 4175331
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 21 deletions.
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 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, 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 install 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

0 comments on commit 4175331

Please sign in to comment.