mirror of https://github.com/astral-sh/uv
Add support for `uv export --all-packages` (#8742)
## Summary Same as the other PRs, but for `uv export`.
This commit is contained in:
parent
3808b61fc1
commit
b36ae6d5ae
|
|
@ -3343,10 +3343,20 @@ pub struct ExportArgs {
|
||||||
#[arg(long, value_enum, default_value_t = ExportFormat::default())]
|
#[arg(long, value_enum, default_value_t = ExportFormat::default())]
|
||||||
pub format: ExportFormat,
|
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.
|
/// Export the dependencies for a specific package in the workspace.
|
||||||
///
|
///
|
||||||
/// If the workspace member does not exist, uv will exit with an error.
|
/// 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>,
|
pub package: Option<PackageName>,
|
||||||
|
|
||||||
/// Include optional dependencies from the specified extra name.
|
/// Include optional dependencies from the specified extra name.
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,17 @@ use petgraph::{Directed, Graph};
|
||||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||||
use url::Url;
|
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_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions};
|
||||||
use uv_distribution_filename::{DistExtension, SourceDistExtension};
|
use uv_distribution_filename::{DistExtension, SourceDistExtension};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_git::GitReference;
|
use uv_git::GitReference;
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::ExtraName;
|
||||||
use uv_pep508::MarkerTree;
|
use uv_pep508::MarkerTree;
|
||||||
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
|
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
|
||||||
|
use uv_workspace::InstallTarget;
|
||||||
use crate::graph_ops::marker_reachability;
|
|
||||||
use crate::lock::{Package, PackageId, Source};
|
|
||||||
use crate::{Lock, LockError};
|
|
||||||
|
|
||||||
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
|
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ pub struct RequirementsTxtExport<'lock> {
|
||||||
impl<'lock> RequirementsTxtExport<'lock> {
|
impl<'lock> RequirementsTxtExport<'lock> {
|
||||||
pub fn from_lock(
|
pub fn from_lock(
|
||||||
lock: &'lock Lock,
|
lock: &'lock Lock,
|
||||||
root_name: &PackageName,
|
target: InstallTarget<'lock>,
|
||||||
extras: &ExtrasSpecification,
|
extras: &ExtrasSpecification,
|
||||||
dev: &DevGroupsManifest,
|
dev: &DevGroupsManifest,
|
||||||
editable: EditableMode,
|
editable: EditableMode,
|
||||||
|
|
@ -52,6 +52,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
|
||||||
let root = petgraph.add_node(Node::Root);
|
let root = petgraph.add_node(Node::Root);
|
||||||
|
|
||||||
// Add the workspace package to the queue.
|
// Add the workspace package to the queue.
|
||||||
|
for root_name in target.packages() {
|
||||||
let dist = lock
|
let dist = lock
|
||||||
.find_by_name(root_name)
|
.find_by_name(root_name)
|
||||||
.expect("found too many packages matching root")
|
.expect("found too many packages matching root")
|
||||||
|
|
@ -115,6 +116,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create all the relevant nodes.
|
// Create all the relevant nodes.
|
||||||
while let Some((package, extra)) = queue.pop_front() {
|
while let Some((package, extra)) = queue.pop_front() {
|
||||||
|
|
@ -170,7 +172,11 @@ impl<'lock> RequirementsTxtExport<'lock> {
|
||||||
Node::Package(package) => Some((index, package)),
|
Node::Package(package) => Some((index, package)),
|
||||||
})
|
})
|
||||||
.filter(|(_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 {
|
.map(|(index, package)| Requirement {
|
||||||
package,
|
package,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ use crate::settings::ResolverSettings;
|
||||||
pub(crate) async fn export(
|
pub(crate) async fn export(
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
format: ExportFormat,
|
format: ExportFormat,
|
||||||
|
all_packages: bool,
|
||||||
package: Option<PackageName>,
|
package: Option<PackageName>,
|
||||||
hashes: bool,
|
hashes: bool,
|
||||||
install_options: InstallOptions,
|
install_options: InstallOptions,
|
||||||
|
|
@ -52,14 +53,7 @@ pub(crate) async fn export(
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
// Identify the project.
|
// Identify the project.
|
||||||
let project = if let Some(package) = package {
|
let project = if frozen && !all_packages {
|
||||||
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 {
|
|
||||||
VirtualProject::discover(
|
VirtualProject::discover(
|
||||||
project_dir,
|
project_dir,
|
||||||
&DiscoveryOptions {
|
&DiscoveryOptions {
|
||||||
|
|
@ -68,15 +62,18 @@ pub(crate) async fn export(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?
|
.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 {
|
} else {
|
||||||
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine the default groups to include.
|
if project.is_non_project() {
|
||||||
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
|
|
||||||
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
|
||||||
|
|
||||||
let VirtualProject::Project(project) = project else {
|
|
||||||
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"));
|
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()),
|
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.
|
// Write the resolved dependencies to the output channel.
|
||||||
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
|
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 => {
|
ExportFormat::RequirementsTxt => {
|
||||||
let export = RequirementsTxtExport::from_lock(
|
let export = RequirementsTxtExport::from_lock(
|
||||||
&lock,
|
&lock,
|
||||||
project.project_name(),
|
target,
|
||||||
&extras,
|
&extras,
|
||||||
&dev.with_defaults(defaults),
|
&dev.with_defaults(defaults),
|
||||||
editable,
|
editable,
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,14 @@ pub(crate) async fn sync(
|
||||||
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
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.
|
// Identify the target.
|
||||||
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
|
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
|
||||||
InstallTarget::frozen(&project, package)
|
InstallTarget::frozen(&project, package)
|
||||||
|
|
@ -90,14 +98,6 @@ pub(crate) async fn sync(
|
||||||
InstallTarget::from_project(&project)
|
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.
|
// Determine the default groups to include.
|
||||||
validate_dependency_groups(target, &dev)?;
|
validate_dependency_groups(target, &dev)?;
|
||||||
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
||||||
|
|
|
||||||
|
|
@ -1510,6 +1510,7 @@ async fn run_project(
|
||||||
commands::export(
|
commands::export(
|
||||||
project_dir,
|
project_dir,
|
||||||
args.format,
|
args.format,
|
||||||
|
args.all_packages,
|
||||||
args.package,
|
args.package,
|
||||||
args.hashes,
|
args.hashes,
|
||||||
args.install_options,
|
args.install_options,
|
||||||
|
|
|
||||||
|
|
@ -1094,6 +1094,7 @@ impl TreeSettings {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ExportSettings {
|
pub(crate) struct ExportSettings {
|
||||||
pub(crate) format: ExportFormat,
|
pub(crate) format: ExportFormat,
|
||||||
|
pub(crate) all_packages: bool,
|
||||||
pub(crate) package: Option<PackageName>,
|
pub(crate) package: Option<PackageName>,
|
||||||
pub(crate) extras: ExtrasSpecification,
|
pub(crate) extras: ExtrasSpecification,
|
||||||
pub(crate) dev: DevGroupsSpecification,
|
pub(crate) dev: DevGroupsSpecification,
|
||||||
|
|
@ -1115,6 +1116,7 @@ impl ExportSettings {
|
||||||
pub(crate) fn resolve(args: ExportArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
pub(crate) fn resolve(args: ExportArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||||
let ExportArgs {
|
let ExportArgs {
|
||||||
format,
|
format,
|
||||||
|
all_packages,
|
||||||
package,
|
package,
|
||||||
extra,
|
extra,
|
||||||
all_extras,
|
all_extras,
|
||||||
|
|
@ -1143,8 +1145,9 @@ impl ExportSettings {
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
package,
|
|
||||||
format,
|
format,
|
||||||
|
all_packages,
|
||||||
|
package,
|
||||||
extras: ExtrasSpecification::from_args(
|
extras: ExtrasSpecification::from_args(
|
||||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||||
extra.unwrap_or_default(),
|
extra.unwrap_or_default(),
|
||||||
|
|
|
||||||
|
|
@ -503,6 +503,105 @@ fn non_root() -> Result<()> {
|
||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn relative_path() -> Result<()> {
|
fn relative_path() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
|
||||||
|
|
@ -2034,6 +2034,12 @@ uv export [OPTIONS]
|
||||||
|
|
||||||
<dl class="cli-reference"><dt><code>--all-extras</code></dt><dd><p>Include all optional dependencies</p>
|
<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>
|
</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>
|
<p>Can be provided multiple times.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue