mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-25 05:33:43 +00:00
Add --no-emit-project and friends to uv export (#7110)
## Summary Like `uv sync`, you can omit the current project (`--no-emit-project`), a specific package (`--no-emit-package`), or the entire workspace (`--no-emit-workspace`). Closes https://github.com/astral-sh/uv/issues/6960. Closes #6995.
This commit is contained in:
parent
d0f9016eda
commit
6ae005b0d0
8 changed files with 231 additions and 21 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NodeIndex, MarkerTree>,
|
||||
nodes: Vec<Node<'lock>>,
|
||||
hashes: bool,
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -123,28 +128,33 @@ impl<'lock> RequirementsTxtExport<'lock> {
|
|||
}
|
||||
}
|
||||
|
||||
let reachability = marker_reachability(&petgraph, &[]);
|
||||
let mut reachability = marker_reachability(&petgraph, &[]);
|
||||
|
||||
Ok(Self {
|
||||
graph: petgraph,
|
||||
reachability,
|
||||
hashes,
|
||||
// Collect all packages.
|
||||
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.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 {
|
||||
// Collect all packages.
|
||||
let mut nodes = self.graph.node_references().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))
|
||||
});
|
||||
|
||||
// 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}")?;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PackageName>,
|
||||
hashes: bool,
|
||||
install_options: InstallOptions,
|
||||
output_file: Option<PathBuf>,
|
||||
extras: ExtrasSpecification,
|
||||
dev: bool,
|
||||
|
|
@ -125,6 +126,7 @@ pub(crate) async fn export(
|
|||
&extras,
|
||||
&dev,
|
||||
hashes,
|
||||
&install_options,
|
||||
)?;
|
||||
writeln!(
|
||||
writer,
|
||||
|
|
|
|||
|
|
@ -1323,6 +1323,7 @@ async fn run_project(
|
|||
args.format,
|
||||
args.package,
|
||||
args.hashes,
|
||||
args.install_options,
|
||||
args.output_file,
|
||||
args.extras,
|
||||
args.dev,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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’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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue