Skip to content
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
10 changes: 5 additions & 5 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We want the plural too in the docstring above Export(ExportArgs),

///
/// 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.
///
Expand Down
78 changes: 51 additions & 27 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
};
Expand Down Expand Up @@ -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 {
workspace: project.workspace(),
name: project.project_name(),
lock: &lock,
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,
},
}
}
}
Expand All @@ -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 {
// By default, install the entire workspace.
InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
match package.as_slice() {
// By default, install the entire workspace.
[] => InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
},
[name] => InstallTarget::Project {
workspace,
name,
lock: &lock,
},
names => InstallTarget::Projects {
workspace,
names,
lock: &lock,
},
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
144 changes: 144 additions & 0 deletions crates/uv/tests/it/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
6 changes: 3 additions & 3 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -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>
Expand Down
Loading