diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 243ecc3a79a7..1fb2c841f6e5 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -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: &InstallTarget, + project: InstallTarget<'_>, marker_env: &ResolverMarkerEnvironment, tags: &Tags, extras: &ExtrasSpecification, @@ -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)); @@ -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)); @@ -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. diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 007807da98aa..a8107b0e2426 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -874,11 +874,6 @@ impl ProjectWorkspace { &self.workspace } - /// Convert the [`ProjectWorkspace`] into a [`Workspace`]. - pub fn into_workspace(self) -> Workspace { - self.workspace - } - /// Returns the current project as a [`WorkspaceMember`]. pub fn current_project(&self) -> &WorkspaceMember { &self.workspace().packages[&self.project_name] @@ -1369,39 +1364,20 @@ impl VirtualProject { } /// A target that can be installed. -/// -/// The project could be a package within a workspace, a real workspace root, a frozen member within -/// a workspace or a (legacy) non-project workspace root, which can define its own dev dependencies. -#[derive(Debug, Clone)] -pub enum InstallTarget { +#[derive(Debug, Clone, Copy)] +pub enum InstallTarget<'env> { /// A project (which could be a workspace root or member). - Project(ProjectWorkspace), + Project(&'env ProjectWorkspace), /// A (legacy) non-project workspace root. - NonProject(Workspace), + NonProject(&'env Workspace), /// A frozen member within a [`Workspace`]. - FrozenMember(Workspace, PackageName), + FrozenMember(&'env Workspace, &'env PackageName), } -impl InstallTarget { - /// Find the current [`InstallTarget`], given the current directory. - pub async fn discover( - path: &Path, - options: &DiscoveryOptions<'_>, - ) -> Result { - match VirtualProject::discover(path, options).await? { - VirtualProject::Project(project) => Ok(InstallTarget::Project(project)), - VirtualProject::NonProject(workspace) => Ok(InstallTarget::NonProject(workspace)), - } - } - - /// Set the [`InstallTarget`] to a frozen member within the workspace. - #[must_use] - pub fn with_frozen_member(self, package_name: PackageName) -> Self { - match self { - Self::Project(project) => Self::FrozenMember(project.into_workspace(), package_name), - Self::NonProject(workspace) => Self::FrozenMember(workspace, package_name), - Self::FrozenMember(workspace, _) => Self::FrozenMember(workspace, package_name), - } +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. @@ -1418,7 +1394,7 @@ impl InstallTarget { match self { 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)), + Self::FrozenMember(_, package_name) => Either::Left(std::iter::once(*package_name)), } } @@ -1468,8 +1444,8 @@ impl InstallTarget { } } -impl From for InstallTarget { - fn from(project: VirtualProject) -> Self { +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), diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 33fdea382b4e..158bbb74686b 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -668,7 +668,7 @@ pub(crate) async fn add( let install_options = InstallOptions::default(); if let Err(err) = project::sync::do_sync( - &InstallTarget::from(project.clone()), + InstallTarget::from(&project), &venv, &lock, &extras, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index ed8624e621bd..e7644354652f 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -196,7 +196,7 @@ pub(crate) async fn remove( let state = SharedState::default(); project::sync::do_sync( - &InstallTarget::from(project.clone()), + InstallTarget::from(&project), &venv, &lock, &extras, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index fa41acc04759..4047f6c48483 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -441,7 +441,7 @@ pub(crate) async fn run( let install_options = InstallOptions::default(); project::sync::do_sync( - &InstallTarget::from(project.clone()), + InstallTarget::from(&project), &venv, result.lock(), &extras, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 9cd69244c336..9d02808dd00d 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -14,7 +14,7 @@ use uv_normalize::{PackageName, DEV_DEPENDENCIES}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Lock}; use uv_types::{BuildIsolation, HashStrategy}; -use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, Workspace}; +use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations::Modifications; @@ -44,30 +44,32 @@ pub(crate) async fn sync( cache: &Cache, printer: Printer, ) -> Result { - // Identify the target. - let target = if frozen { - let project = InstallTarget::discover( + // Identify the project. + let project = if frozen { + VirtualProject::discover( &CWD, &DiscoveryOptions { members: MemberDiscovery::None, ..DiscoveryOptions::default() }, ) - .await?; - if let Some(package) = package { - project.with_frozen_member(package) - } else { - project - } - } else if let Some(package) = package { - InstallTarget::Project( + .await? + } else if let Some(package) = package.as_ref() { + VirtualProject::Project( Workspace::discover(&CWD, &DiscoveryOptions::default()) .await? .with_current_project(package.clone()) .with_context(|| format!("Package `{package}` not found in workspace"))?, ) } else { - InstallTarget::discover(&CWD, &DiscoveryOptions::default()).await? + VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await? + }; + + // Identify the target. + let target = if let Some(package) = package.as_ref().filter(|_| frozen) { + InstallTarget::frozen_member(&project, package) + } else { + InstallTarget::from(&project) }; // Discover or create the virtual environment. @@ -114,7 +116,7 @@ pub(crate) async fn sync( // Perform the sync operation. do_sync( - &target, + target, &venv, &lock, &extras, @@ -138,7 +140,7 @@ pub(crate) async fn sync( /// Sync a lockfile with an environment. #[allow(clippy::fn_params_excessive_bools)] pub(super) async fn do_sync( - target: &InstallTarget, + target: InstallTarget<'_>, venv: &PythonEnvironment, lock: &Lock, extras: &ExtrasSpecification, diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 7a1532cfbeef..b699079cff14 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -1125,7 +1125,7 @@ fn no_install_workspace() -> Result<()> { + sniffio==1.3.1 "###); - // Unless `--package` is used. + // Even if `--package` is used. uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--no-install-workspace").arg("--frozen"), @r###" success: true exit_code: 0 @@ -1138,6 +1138,16 @@ fn no_install_workspace() -> Result<()> { - sniffio==1.3.1 "###); + // Unless the package doesn't exist. + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("fake").arg("--no-install-workspace").arg("--frozen"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: could not find root package `fake` + "###); + // But we do require the root `pyproject.toml`. fs_err::remove_file(context.temp_dir.join("pyproject.toml"))?;