Skip to content

Support uv export for non-project workspaces #10144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 24, 2024
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
4 changes: 2 additions & 2 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ impl Lock {
.iter()
.copied()
.map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
.filter_map(super::requires_python::SimplifiedMarkerTree::try_to_string),
.filter_map(SimplifiedMarkerTree::try_to_string),
);
doc.insert("supported-markers", value(supported_environments));
}
Expand Down Expand Up @@ -3567,7 +3567,7 @@ struct Dependency {
/// by assuming `requires-python` is satisfied. So if
/// `requires-python = '>=3.8'`, then
/// `python_version >= '3.8' and python_version < '3.12'`
/// gets simplfiied to `python_version < '3.12'`.
/// gets simplified to `python_version < '3.12'`.
///
/// Generally speaking, this marker should not be exposed to
/// anything outside this module unless it's for a specialized use
Expand Down
96 changes: 94 additions & 2 deletions crates/uv-resolver/src/lock/requirements_txt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ 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::{LockErrorKind, Package, PackageId, Source};
use crate::universal_marker::{ConflictMarker, UniversalMarker};
use crate::{InstallTarget, LockError};

Expand Down Expand Up @@ -50,7 +50,7 @@ impl<'lock> RequirementsTxtExport<'lock> {

let root = petgraph.add_node(Node::Root);

// Add the workspace package to the queue.
// Add the workspace packages to the queue.
for root_name in target.packages() {
if prune.contains(root_name) {
continue;
Expand Down Expand Up @@ -135,6 +135,98 @@ impl<'lock> RequirementsTxtExport<'lock> {
}
}

// Add requirements that are exclusive to the workspace root (e.g., dependency groups in
// (legacy) non-project workspace roots).
let root_groups = target
.groups()
.map_err(|err| LockErrorKind::DependencyGroup { err })?;
let root_requirements = {
root_groups
.iter()
.filter_map(|(group, deps)| {
if dev.contains(group) {
Some(deps)
} else {
None
}
})
.flatten()
.filter(|dep| !prune.contains(&dep.name))
.collect::<Vec<_>>()
};

// Index the lockfile by package name, to avoid making multiple passes over the lockfile.
if !root_requirements.is_empty() {
let by_name: FxHashMap<_, Vec<_>> = {
let names = root_requirements
.iter()
.map(|dep| &dep.name)
.collect::<FxHashSet<_>>();
target.lock().packages().iter().fold(
FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher),
|mut map, package| {
if names.contains(&package.id.name) {
map.entry(&package.id.name).or_default().push(package);
}
map
},
)
};

for requirement in root_requirements {
for dist in by_name.get(&requirement.name).into_iter().flatten() {
// Determine whether this entry is "relevant" for the requirement, by intersecting
// the markers.
let marker = if dist.fork_markers.is_empty() {
requirement.marker
} else {
let mut combined = MarkerTree::FALSE;
for fork_marker in &dist.fork_markers {
combined.or(fork_marker.pep508());
}
combined.and(requirement.marker);
combined
};

// Simplify the marker.
let marker = target.lock().simplify_environment(marker);

// Add the dependency 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 dep_index = inverse[&dist.id];
petgraph.add_edge(
root,
dep_index,
UniversalMarker::new(
marker,
// OK because we've verified above that we do not have any
// conflicting extras/groups.
//
// So why use `UniversalMarker`? Because that's what
// `marker_reachability` wants and it (probably) isn't
// worth inventing a new abstraction so that it can accept
// graphs with either `MarkerTree` or `UniversalMarker`.
ConflictMarker::TRUE,
),
);

// Push its dependencies on the queue.
if seen.insert((&dist.id, None)) {
queue.push_back((dist, None));
}
for extra in &requirement.extras {
if seen.insert((&dist.id, Some(extra))) {
queue.push_back((dist, Some(extra)));
}
}
}
}
}

// Create all the relevant nodes.
while let Some((package, extra)) = queue.pop_front() {
let index = inverse[&package.id];
Expand Down
71 changes: 50 additions & 21 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,23 @@ pub(crate) async fn export(
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};

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"));
};

// Validate that any referenced dependency groups are defined in the workspace.
if !frozen {
let target = if all_packages {
DependencyGroupsTarget::Workspace(project.workspace())
} else {
DependencyGroupsTarget::Project(project)
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
DependencyGroupsTarget::Workspace(project.workspace())
} else {
DependencyGroupsTarget::Project(project)
}
}
VirtualProject::NonProject(workspace) => DependencyGroupsTarget::Workspace(workspace),
};
target.validate(&dev)?;
}

// Determine the default groups to include.
let defaults = default_dependency_groups(project.current_project().pyproject_toml())?;
let defaults = default_dependency_groups(project.pyproject_toml())?;
let dev = dev.with_defaults(defaults);

// Determine the lock mode.
Expand Down Expand Up @@ -163,19 +164,47 @@ pub(crate) async fn export(
detect_conflicts(&lock, &extras, &dev)?;

// Identify the installation target.
let target = if all_packages {
InstallTarget::Workspace {
workspace: project.workspace(),
lock: &lock,
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
InstallTarget::Workspace {
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,
}
}
}
} else {
InstallTarget::Project {
workspace: project.workspace(),
// If `--frozen --package` is specified, and only the root `pyproject.toml` was
// discovered, the child won't be present in the workspace; but we _know_ that
// we want to install it, so we override the package name.
name: package.as_ref().unwrap_or(project.project_name()),
lock: &lock,
VirtualProject::NonProject(workspace) => {
if all_packages {
InstallTarget::NonProjectWorkspace {
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,
}
}
}
};

Expand Down
Loading
Loading