Accept multiple packages in uv export (#16603)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | aarch64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions
CI / integration test | pyenv on wsl x86-64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 10 (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | x86-64 python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | activate nushell venv (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / build binary | msrv (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | windows python install manager (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
zizmor / Run zizmor (push) Waiting to run

## Summary

Closes https://github.com/astral-sh/uv/issues/16503.
This commit is contained in:
Charlie Marsh 2025-11-05 17:52:22 -05:00 committed by GitHub
parent 148b694b6b
commit 5fe8af114b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 204 additions and 36 deletions

View file

@ -1035,8 +1035,8 @@ pub enum ProjectCommand {
/// uv will search for a project in the current directory or any parent directory. If a project
/// cannot be found, uv will exit with an error.
///
/// If operating in a workspace, the root will be exported by default; however, a specific
/// member can be selected using the `--package` option.
/// If operating in a workspace, the root will be exported by default; however, specific
/// members can be selected using the `--package` option.
#[command(
after_help = "Use `uv help export` for more details.",
after_long_help = ""
@ -4320,11 +4320,11 @@ pub struct ExportArgs {
#[arg(long, conflicts_with = "package")]
pub all_packages: bool,
/// Export the dependencies for a specific package in the workspace.
/// Export the dependencies for specific packages in the workspace.
///
/// If the workspace member does not exist, uv will exit with an error.
/// If any workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with = "all_packages")]
pub package: Option<PackageName>,
pub package: Vec<PackageName>,
/// Prune the given package from the dependency tree.
///

View file

@ -57,7 +57,7 @@ pub(crate) async fn export(
project_dir: &Path,
format: Option<ExportFormat>,
all_packages: bool,
package: Option<PackageName>,
package: Vec<PackageName>,
prune: Vec<PackageName>,
hashes: bool,
install_options: InstallOptions,
@ -98,16 +98,28 @@ pub(crate) async fn export(
&workspace_cache,
)
.await?
} else if let Some(package) = package.as_ref() {
} else if let [name] = package.as_slice() {
VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
.with_current_project(name.clone())
.with_context(|| format!("Package `{name}` not found in workspace"))?,
)
} else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?
let project = VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&workspace_cache,
)
.await?;
for name in &package {
if !project.workspace().packages().contains_key(name) {
return Err(anyhow::anyhow!("Package `{name}` not found in workspace"));
}
}
project
};
ExportTarget::Project(project)
};
@ -219,18 +231,24 @@ pub(crate) async fn export(
workspace: project.workspace(),
lock: &lock,
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace: project.workspace(),
name: package,
lock: &lock,
}
} else {
// By default, install the root package.
InstallTarget::Project {
match package.as_slice() {
// By default, install the root project.
[] => InstallTarget::Project {
workspace: project.workspace(),
name: project.project_name(),
lock: &lock,
},
[name] => InstallTarget::Project {
workspace: project.workspace(),
name,
lock: &lock,
},
names => InstallTarget::Projects {
workspace: project.workspace(),
names,
lock: &lock,
},
}
}
}
@ -240,17 +258,23 @@ pub(crate) async fn export(
workspace,
lock: &lock,
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace,
name: package,
lock: &lock,
}
} else {
match package.as_slice() {
// By default, install the entire workspace.
InstallTarget::NonProjectWorkspace {
[] => InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
},
[name] => InstallTarget::Project {
workspace,
name,
lock: &lock,
},
names => InstallTarget::Projects {
workspace,
names,
lock: &lock,
},
}
}
}

View file

@ -1965,7 +1965,7 @@ impl TreeSettings {
pub(crate) struct ExportSettings {
pub(crate) format: Option<ExportFormat>,
pub(crate) all_packages: bool,
pub(crate) package: Option<PackageName>,
pub(crate) package: Vec<PackageName>,
pub(crate) prune: Vec<PackageName>,
pub(crate) extras: ExtrasSpecification,
pub(crate) groups: DependencyGroups,

View file

@ -4660,3 +4660,147 @@ fn export_lock_workspace_mismatch_with_frozen() -> Result<()> {
Ok(())
}
/// Export multiple packages within a workspace.
#[test]
fn multiple_packages() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["foo", "bar", "baz"]
[tool.uv.sources]
foo = { workspace = true }
bar = { workspace = true }
baz = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
"#,
)?;
context
.temp_dir
.child("packages")
.child("foo")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio"]
"#,
)?;
context
.temp_dir
.child("packages")
.child("bar")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "bar"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
"#,
)?;
context
.temp_dir
.child("packages")
.child("baz")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "baz"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
context.lock().assert().success();
// Export `foo` and `bar`.
uv_snapshot!(context.filters(), context.export()
.arg("--package").arg("foo")
.arg("--package").arg("bar"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR] --package foo --package bar
-e ./packages/bar
-e ./packages/foo
anyio==4.3.0 \
--hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \
--hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6
# via foo
idna==3.6 \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
# via anyio
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via anyio
typing-extensions==4.10.0 \
--hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \
--hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb
# via bar
----- stderr -----
Resolved 9 packages in [TIME]
"###);
// Export `foo`, `bar`, and `baz`.
uv_snapshot!(context.filters(), context.export()
.arg("--package").arg("foo")
.arg("--package").arg("bar")
.arg("--package").arg("baz"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR] --package foo --package bar --package baz
-e ./packages/bar
-e ./packages/baz
-e ./packages/foo
anyio==4.3.0 \
--hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \
--hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6
# via foo
idna==3.6 \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
# via anyio
iniconfig==2.0.0 \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
# via baz
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via anyio
typing-extensions==4.10.0 \
--hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \
--hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb
# via bar
----- stderr -----
Resolved 9 packages in [TIME]
"###);
Ok(())
}

View file

@ -1784,7 +1784,7 @@ The project is re-locked before exporting unless the `--locked` or `--frozen` fl
uv will search for a project in the current directory or any parent directory. If a project cannot be found, uv will exit with an error.
If operating in a workspace, the root will be exported by default; however, a specific member can be selected using the `--package` option.
If operating in a workspace, the root will be exported by default; however, specific members can be selected using the `--package` option.
<h3 class="cli-reference">Usage</h3>
@ -1944,8 +1944,8 @@ uv export [OPTIONS]
<p>The project and its dependencies will be omitted.</p>
<p>May be provided multiple times. Implies <code>--no-default-groups</code>.</p>
</dd><dt id="uv-export--output-file"><a href="#uv-export--output-file"><code>--output-file</code></a>, <code>-o</code> <i>output-file</i></dt><dd><p>Write the exported requirements to the given file</p>
</dd><dt id="uv-export--package"><a href="#uv-export--package"><code>--package</code></a> <i>package</i></dt><dd><p>Export the dependencies for a specific package in the workspace.</p>
<p>If the workspace member does not exist, uv will exit with an error.</p>
</dd><dt id="uv-export--package"><a href="#uv-export--package"><code>--package</code></a> <i>package</i></dt><dd><p>Export the dependencies for specific packages in the workspace.</p>
<p>If any workspace member does not exist, uv will exit with an error.</p>
</dd><dt id="uv-export--prerelease"><a href="#uv-export--prerelease"><code>--prerelease</code></a> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
<p>By default, uv will accept pre-releases for packages that <em>only</em> publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (<code>if-necessary-or-explicit</code>).</p>
<p>May also be set with the <code>UV_PRERELEASE</code> environment variable.</p><p>Possible values:</p>