Skip to content

Commit

Permalink
Allow uv sync --package without copying member pyproject.toml (#6943
Browse files Browse the repository at this point in the history
)

## Summary

Closes #6935.
  • Loading branch information
charliermarsh committed Sep 2, 2024
1 parent 6897001 commit f9c0458
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 59 deletions.
36 changes: 29 additions & 7 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use uv_fs::{relative_to, PortablePath, PortablePathBuf};
use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_types::BuildContext;
use uv_workspace::{VirtualProject, Workspace};
use uv_workspace::{InstallTarget, Workspace};

pub use crate::lock::requirements_txt::RequirementsTxtExport;
pub use crate::lock::tree::TreeDisplay;
Expand Down Expand Up @@ -426,7 +426,7 @@ impl Lock {
/// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
pub fn to_resolution(
&self,
project: &VirtualProject,
project: InstallTarget<'_>,
marker_env: &ResolverMarkerEnvironment,
tags: &Tags,
extras: &ExtrasSpecification,
Expand All @@ -439,8 +439,12 @@ impl Lock {
for root_name in project.packages() {
let root = self
.find_by_name(root_name)
.expect("found too many packages matching root")
.expect("could not find root");
.map_err(|_| LockErrorKind::MultipleRootPackages {
name: root_name.clone(),
})?
.ok_or_else(|| LockErrorKind::MissingRootPackage {
name: root_name.clone(),
})?;

// Add the base package.
queue.push_back((root, None));
Expand All @@ -466,10 +470,15 @@ impl Lock {
for group in dev {
for dependency in project.group(group) {
if dependency.marker.evaluate(marker_env, &[]) {
let root_name = &dependency.name;
let root = self
.find_by_markers(&dependency.name, marker_env)
.expect("found too many packages matching root")
.expect("could not find root");
.find_by_markers(root_name, marker_env)
.map_err(|_| LockErrorKind::MultipleRootPackages {
name: root_name.clone(),
})?
.ok_or_else(|| LockErrorKind::MissingRootPackage {
name: root_name.clone(),
})?;

// Add the base package.
queue.push_back((root, None));
Expand Down Expand Up @@ -3605,6 +3614,19 @@ enum LockErrorKind {
/// An error that occurs when converting a URL to a path
#[error("failed to convert URL to path")]
UrlToPath,
/// An error that occurs when multiple packages with the same
/// name were found when identifying the root packages.
#[error("found multiple packages matching `{name}`")]
MultipleRootPackages {
/// The ID of the package.
name: PackageName,
},
/// An error that occurs when a root package can't be found.
#[error("could not find root package `{name}`")]
MissingRootPackage {
/// The ID of the package.
name: PackageName,
},
}

/// An error that occurs when a source string could not be parsed.
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-workspace/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub use workspace::{
check_nested_workspaces, DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject,
Workspace, WorkspaceError, WorkspaceMember,
check_nested_workspaces, DiscoveryOptions, InstallTarget, MemberDiscovery, ProjectWorkspace,
VirtualProject, Workspace, WorkspaceError, WorkspaceMember,
};

pub mod pyproject;
Expand Down
96 changes: 70 additions & 26 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,7 @@ fn is_included_in_workspace(
Ok(false)
}

/// A project that can be synced.
/// A project that can be discovered.
///
/// The project could be a package within a workspace, a real workspace root, or a (legacy)
/// non-project workspace root, which can define its own dev dependencies.
Expand Down Expand Up @@ -1311,13 +1311,13 @@ impl VirtualProject {
#[must_use]
pub fn with_pyproject_toml(self, pyproject_toml: PyProjectToml) -> Option<Self> {
match self {
VirtualProject::Project(project) => Some(VirtualProject::Project(
project.with_pyproject_toml(pyproject_toml)?,
)),
VirtualProject::NonProject(workspace) => {
Self::Project(project) => {
Some(Self::Project(project.with_pyproject_toml(pyproject_toml)?))
}
Self::NonProject(workspace) => {
// If this is a non-project workspace root, then by definition the root isn't a
// member, so we can just update the top-level `pyproject.toml`.
Some(VirtualProject::NonProject(Workspace {
Some(Self::NonProject(Workspace {
pyproject_toml,
..workspace.clone()
}))
Expand All @@ -1328,38 +1328,77 @@ impl VirtualProject {
/// Return the root of the project.
pub fn root(&self) -> &Path {
match self {
VirtualProject::Project(project) => project.project_root(),
VirtualProject::NonProject(workspace) => workspace.install_path(),
Self::Project(project) => project.project_root(),
Self::NonProject(workspace) => workspace.install_path(),
}
}

/// Return the [`PyProjectToml`] of the project.
pub fn pyproject_toml(&self) -> &PyProjectToml {
match self {
VirtualProject::Project(project) => project.current_project().pyproject_toml(),
VirtualProject::NonProject(workspace) => &workspace.pyproject_toml,
Self::Project(project) => project.current_project().pyproject_toml(),
Self::NonProject(workspace) => &workspace.pyproject_toml,
}
}

/// Return the [`Workspace`] of the project.
pub fn workspace(&self) -> &Workspace {
match self {
VirtualProject::Project(project) => project.workspace(),
VirtualProject::NonProject(workspace) => workspace,
Self::Project(project) => project.workspace(),
Self::NonProject(workspace) => workspace,
}
}

/// Return the [`PackageName`] of the project, if available.
pub fn project_name(&self) -> Option<&PackageName> {
match self {
VirtualProject::Project(project) => Some(project.project_name()),
VirtualProject::NonProject(_) => None,
}
}

/// Return the [`PackageName`] of the project.
/// Returns `true` if the project is a virtual workspace root.
pub fn is_non_project(&self) -> bool {
matches!(self, VirtualProject::NonProject(_))
}
}

/// A target that can be installed.
#[derive(Debug, Clone, Copy)]
pub enum InstallTarget<'env> {
/// A project (which could be a workspace root or member).
Project(&'env ProjectWorkspace),
/// A (legacy) non-project workspace root.
NonProject(&'env Workspace),
/// A frozen member within a [`Workspace`].
FrozenMember(&'env Workspace, &'env PackageName),
}

impl<'env> InstallTarget<'env> {
/// Create an [`InstallTarget`] for a frozen member within a workspace.
pub fn frozen_member(project: &'env VirtualProject, package_name: &'env PackageName) -> Self {
Self::FrozenMember(project.workspace(), package_name)
}

/// Return the [`Workspace`] of the target.
pub fn workspace(&self) -> &Workspace {
match self {
Self::Project(project) => project.workspace(),
Self::NonProject(workspace) => workspace,
Self::FrozenMember(workspace, _) => workspace,
}
}

/// Return the [`PackageName`] of the target.
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
match self {
VirtualProject::Project(project) => {
Either::Left(std::iter::once(project.project_name()))
}
VirtualProject::NonProject(workspace) => Either::Right(workspace.packages().keys()),
Self::Project(project) => Either::Left(std::iter::once(project.project_name())),
Self::NonProject(workspace) => Either::Right(workspace.packages().keys()),
Self::FrozenMember(_, package_name) => Either::Left(std::iter::once(*package_name)),
}
}

/// Return the [`VirtualProject`] dependencies for the given group name.
/// Return the [`InstallTarget`] dependencies for the given group name.
///
/// Returns dependencies that apply to the workspace root, but not any of its members. As such,
/// only returns a non-empty iterator for virtual workspaces, which can include dev dependencies
Expand All @@ -1369,11 +1408,11 @@ impl VirtualProject {
name: &GroupName,
) -> impl Iterator<Item = &pep508_rs::Requirement<VerbatimParsedUrl>> {
match self {
VirtualProject::Project(_) => {
Self::Project(_) | Self::FrozenMember(..) => {
// For projects, dev dependencies are attached to the members.
Either::Left(std::iter::empty())
}
VirtualProject::NonProject(workspace) => {
Self::NonProject(workspace) => {
// For non-projects, we might have dev dependencies that are attached to the
// workspace root (which isn't a member).
if name == &*DEV_DEPENDENCIES {
Expand All @@ -1395,17 +1434,22 @@ impl VirtualProject {
}
}

/// Return the [`PackageName`] of the project, if available.
/// Return the [`PackageName`] of the target, if available.
pub fn project_name(&self) -> Option<&PackageName> {
match self {
VirtualProject::Project(project) => Some(project.project_name()),
VirtualProject::NonProject(_) => None,
Self::Project(project) => Some(project.project_name()),
Self::NonProject(_) => None,
Self::FrozenMember(_, package_name) => Some(package_name),
}
}
}

/// Returns `true` if the project is a virtual workspace root.
pub fn is_non_project(&self) -> bool {
matches!(self, VirtualProject::NonProject(_))
impl<'env> From<&'env VirtualProject> for InstallTarget<'env> {
fn from(project: &'env VirtualProject) -> Self {
match project {
VirtualProject::Project(project) => Self::Project(project),
VirtualProject::NonProject(workspace) => Self::NonProject(workspace),
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::{DependencyType, Source, SourceError};
use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace};

use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryResolveLogger,
Expand Down Expand Up @@ -668,7 +668,7 @@ pub(crate) async fn add(
let install_options = InstallOptions::default();

if let Err(err) = project::sync::do_sync(
&project,
InstallTarget::from(&project),
&venv,
&lock,
&extras,
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use uv_scripts::Pep723Script;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace};

use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications;
Expand Down Expand Up @@ -196,7 +196,7 @@ pub(crate) async fn remove(
let state = SharedState::default();

project::sync::do_sync(
&project,
InstallTarget::from(&project),
&venv,
&lock,
&extras,
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use uv_python::{
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_scripts::Pep723Script;
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};
use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace, WorkspaceError};

use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
Expand Down Expand Up @@ -432,7 +432,7 @@ pub(crate) async fn run(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
Expand All @@ -441,7 +441,7 @@ pub(crate) async fn run(
let install_options = InstallOptions::default();

project::sync::do_sync(
&project,
InstallTarget::from(&project),
&venv,
result.lock(),
&extras,
Expand Down
Loading

0 comments on commit f9c0458

Please sign in to comment.