Add support for `uv export --all-packages` (#8742)

## Summary

Same as the other PRs, but for `uv export`.
This commit is contained in:
Charlie Marsh 2024-11-01 22:25:29 -04:00 committed by GitHub
parent 3808b61fc1
commit b36ae6d5ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 219 additions and 84 deletions

View File

@ -3343,10 +3343,20 @@ pub struct ExportArgs {
#[arg(long, value_enum, default_value_t = ExportFormat::default())]
pub format: ExportFormat,
/// Export the entire workspace.
///
/// The dependencies for all workspace members will be included in the
/// exported requirements file.
///
/// Any extras or groups specified via `--extra`, `--group`, or related options
/// will be applied to all workspace members.
#[arg(long, conflicts_with = "package")]
pub all_packages: bool,
/// Export the dependencies for a specific package in the workspace.
///
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long)]
#[arg(long, conflicts_with = "all_packages")]
pub package: Option<PackageName>,
/// Include optional dependencies from the specified extra name.

View File

@ -10,17 +10,17 @@ use petgraph::{Directed, Graph};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use url::Url;
use crate::graph_ops::marker_reachability;
use crate::lock::{Package, PackageId, Source};
use crate::{Lock, LockError};
use uv_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions};
use uv_distribution_filename::{DistExtension, SourceDistExtension};
use uv_fs::Simplified;
use uv_git::GitReference;
use uv_normalize::{ExtraName, PackageName};
use uv_normalize::ExtraName;
use uv_pep508::MarkerTree;
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
use crate::graph_ops::marker_reachability;
use crate::lock::{Package, PackageId, Source};
use crate::{Lock, LockError};
use uv_workspace::InstallTarget;
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
@ -35,7 +35,7 @@ pub struct RequirementsTxtExport<'lock> {
impl<'lock> RequirementsTxtExport<'lock> {
pub fn from_lock(
lock: &'lock Lock,
root_name: &PackageName,
target: InstallTarget<'lock>,
extras: &ExtrasSpecification,
dev: &DevGroupsManifest,
editable: EditableMode,
@ -52,65 +52,67 @@ impl<'lock> RequirementsTxtExport<'lock> {
let root = petgraph.add_node(Node::Root);
// Add the workspace package to the queue.
let dist = lock
.find_by_name(root_name)
.expect("found too many packages matching root")
.expect("could not find root");
for root_name in target.packages() {
let dist = lock
.find_by_name(root_name)
.expect("found too many packages matching root")
.expect("could not find root");
if dev.prod() {
// Add the workspace package to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
entry.insert(petgraph.add_node(Node::Package(dist)));
}
// Add an edge from the root.
let index = inverse[&dist.id];
petgraph.add_edge(root, index, MarkerTree::TRUE);
// Push its dependencies on the queue.
queue.push_back((dist, None));
match extras {
ExtrasSpecification::None => {}
ExtrasSpecification::All => {
for extra in dist.optional_dependencies.keys() {
queue.push_back((dist, Some(extra)));
}
}
ExtrasSpecification::Some(extras) => {
for extra in extras {
queue.push_back((dist, Some(extra)));
}
}
}
}
// Add any development dependencies.
for group in dev.iter() {
for dep in dist.dependency_groups.get(group).into_iter().flatten() {
let dep_dist = lock.find_by_id(&dep.package_id);
// Add the dependency to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
if dev.prod() {
// Add the workspace package to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
entry.insert(petgraph.add_node(Node::Package(dist)));
}
// Add an edge from the root. Development dependencies may be installed without
// installing the workspace package itself (which can never have markers on it
// anyway), so they're directly connected to the root.
let dep_index = inverse[&dep.package_id];
petgraph.add_edge(
root,
dep_index,
dep.simplified_marker.as_simplified_marker_tree().clone(),
);
// Add an edge from the root.
let index = inverse[&dist.id];
petgraph.add_edge(root, index, MarkerTree::TRUE);
// Push its dependencies on the queue.
if seen.insert((&dep.package_id, None)) {
queue.push_back((dep_dist, None));
queue.push_back((dist, None));
match extras {
ExtrasSpecification::None => {}
ExtrasSpecification::All => {
for extra in dist.optional_dependencies.keys() {
queue.push_back((dist, Some(extra)));
}
}
ExtrasSpecification::Some(extras) => {
for extra in extras {
queue.push_back((dist, Some(extra)));
}
}
}
for extra in &dep.extra {
if seen.insert((&dep.package_id, Some(extra))) {
queue.push_back((dep_dist, Some(extra)));
}
// Add any development dependencies.
for group in dev.iter() {
for dep in dist.dependency_groups.get(group).into_iter().flatten() {
let dep_dist = lock.find_by_id(&dep.package_id);
// Add the dependency to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
}
// Add an edge from the root. Development dependencies may be installed without
// installing the workspace package itself (which can never have markers on it
// anyway), so they're directly connected to the root.
let dep_index = inverse[&dep.package_id];
petgraph.add_edge(
root,
dep_index,
dep.simplified_marker.as_simplified_marker_tree().clone(),
);
// Push its dependencies on the queue.
if seen.insert((&dep.package_id, None)) {
queue.push_back((dep_dist, None));
}
for extra in &dep.extra {
if seen.insert((&dep.package_id, Some(extra))) {
queue.push_back((dep_dist, Some(extra)));
}
}
}
}
@ -170,7 +172,11 @@ impl<'lock> RequirementsTxtExport<'lock> {
Node::Package(package) => Some((index, package)),
})
.filter(|(_index, package)| {
install_options.include_package(&package.id.name, Some(root_name), lock.members())
install_options.include_package(
&package.id.name,
target.project_name(),
lock.members(),
)
})
.map(|(index, package)| Requirement {
package,

View File

@ -30,6 +30,7 @@ use crate::settings::ResolverSettings;
pub(crate) async fn export(
project_dir: &Path,
format: ExportFormat,
all_packages: bool,
package: Option<PackageName>,
hashes: bool,
install_options: InstallOptions,
@ -52,14 +53,7 @@ pub(crate) async fn export(
printer: Printer,
) -> Result<ExitStatus> {
// Identify the project.
let project = if let Some(package) = package {
VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
} else if frozen {
let project = if frozen && !all_packages {
VirtualProject::discover(
project_dir,
&DiscoveryOptions {
@ -68,15 +62,18 @@ pub(crate) async fn export(
},
)
.await?
} else if let Some(package) = package.as_ref() {
VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
} else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};
// Determine the default groups to include.
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;
let VirtualProject::Project(project) = project else {
if project.is_non_project() {
return Err(anyhow::anyhow!("Legacy non-project roots are not supported in `uv export`; add a `[project]` table to your `pyproject.toml` to enable exports"));
};
@ -147,6 +144,19 @@ pub(crate) async fn export(
Err(err) => return Err(err.into()),
};
// Identify the target.
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
InstallTarget::frozen(&project, package)
} else if all_packages {
InstallTarget::from_workspace(&project)
} else {
InstallTarget::from_project(&project)
};
// Determine the default groups to include.
validate_dependency_groups(target, &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;
// Write the resolved dependencies to the output channel.
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
@ -155,7 +165,7 @@ pub(crate) async fn export(
ExportFormat::RequirementsTxt => {
let export = RequirementsTxtExport::from_lock(
&lock,
project.project_name(),
target,
&extras,
&dev.with_defaults(defaults),
editable,

View File

@ -81,6 +81,14 @@ pub(crate) async fn sync(
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};
// TODO(lucab): improve warning content
// <https://github.com/astral-sh/uv/issues/7428>
if project.workspace().pyproject_toml().has_scripts()
&& !project.workspace().pyproject_toml().is_package()
{
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
}
// Identify the target.
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
InstallTarget::frozen(&project, package)
@ -90,14 +98,6 @@ pub(crate) async fn sync(
InstallTarget::from_project(&project)
};
// TODO(lucab): improve warning content
// <https://github.com/astral-sh/uv/issues/7428>
if project.workspace().pyproject_toml().has_scripts()
&& !project.workspace().pyproject_toml().is_package()
{
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
}
// Determine the default groups to include.
validate_dependency_groups(target, &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;

View File

@ -1510,6 +1510,7 @@ async fn run_project(
commands::export(
project_dir,
args.format,
args.all_packages,
args.package,
args.hashes,
args.install_options,

View File

@ -1094,6 +1094,7 @@ impl TreeSettings {
#[derive(Debug, Clone)]
pub(crate) struct ExportSettings {
pub(crate) format: ExportFormat,
pub(crate) all_packages: bool,
pub(crate) package: Option<PackageName>,
pub(crate) extras: ExtrasSpecification,
pub(crate) dev: DevGroupsSpecification,
@ -1115,6 +1116,7 @@ impl ExportSettings {
pub(crate) fn resolve(args: ExportArgs, filesystem: Option<FilesystemOptions>) -> Self {
let ExportArgs {
format,
all_packages,
package,
extra,
all_extras,
@ -1143,8 +1145,9 @@ impl ExportSettings {
} = args;
Self {
package,
format,
all_packages,
package,
extras: ExtrasSpecification::from_args(
flag(all_extras, no_all_extras).unwrap_or_default(),
extra.unwrap_or_default(),

View File

@ -503,6 +503,105 @@ fn non_root() -> Result<()> {
Ok(())
}
#[test]
fn all() -> 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();
uv_snapshot!(context.filters(), context.export().arg("--all-packages"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR] --all-packages
-e .
-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]
"###);
Ok(())
}
#[test]
fn non_project() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[tool.uv.workspace]
members = []
[tool.uv]
dev-dependencies = ["anyio"]
"#,
)?;
context.lock().assert().success();
uv_snapshot!(context.filters(), context.export().arg("--dev"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Legacy non-project roots are not supported in `uv export`; add a `[project]` table to your `pyproject.toml` to enable exports
"###);
Ok(())
}
#[test]
fn relative_path() -> Result<()> {
let context = TestContext::new("3.12");

View File

@ -2034,6 +2034,12 @@ uv export [OPTIONS]
<dl class="cli-reference"><dt><code>--all-extras</code></dt><dd><p>Include all optional dependencies</p>
</dd><dt><code>--all-packages</code></dt><dd><p>Export the entire workspace.</p>
<p>The dependencies for all workspace members will be included in the exported requirements file.</p>
<p>Any extras or groups specified via <code>--extra</code>, <code>--group</code>, or related options will be applied to all workspace members.</p>
</dd><dt><code>--allow-insecure-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
<p>Can be provided multiple times.</p>