From b05a39c73516554b4485111ba0131e6d487521fe Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 5 Jun 2024 18:48:19 +0200 Subject: [PATCH] Store explicit project on workspace member (#4048) We know that `[project]` must exist for each workspace member, so we can store it directly and avoid going through the `.and_then()` when we need to access it. This requires cloning the struct due to lack of self-referential structs. An alternative would taking the `Project` from `PyProjectToml` instead, but this could be confusing when passing the `PyProjectToml` around. --- .../src/requirement_lowering.rs | 16 +- crates/uv-distribution/src/workspace.rs | 147 ++++++++++++------ crates/uv/src/commands/project/lock.rs | 7 +- 3 files changed, 111 insertions(+), 59 deletions(-) diff --git a/crates/uv-distribution/src/requirement_lowering.rs b/crates/uv-distribution/src/requirement_lowering.rs index 71bfe6342..e5dbc9e6c 100644 --- a/crates/uv-distribution/src/requirement_lowering.rs +++ b/crates/uv-distribution/src/requirement_lowering.rs @@ -242,15 +242,14 @@ fn path_source( #[cfg(test)] mod test { + use anyhow::Context; use std::path::Path; - use std::str::FromStr; use indoc::indoc; use insta::assert_snapshot; use pypi_types::Metadata23; use uv_configuration::PreviewMode; - use uv_normalize::PackageName; use crate::metadata::Metadata; use crate::pyproject::PyProjectToml; @@ -259,9 +258,16 @@ mod test { async fn metadata_from_pyproject_toml(contents: &str) -> anyhow::Result { let pyproject_toml: PyProjectToml = toml::from_str(contents)?; let path = Path::new("pyproject.toml"); - let project_name = PackageName::from_str("foo").unwrap(); - let project_workspace = - ProjectWorkspace::from_project(path, &pyproject_toml, project_name, Some(path)).await?; + let project_workspace = ProjectWorkspace::from_project( + path, + pyproject_toml + .project + .as_ref() + .context("metadata field project not found")?, + &pyproject_toml, + Some(path), + ) + .await?; let metadata = Metadata23::parse_pyproject_toml(contents)?; Ok(Metadata::from_project_workspace( metadata, diff --git a/crates/uv-distribution/src/workspace.rs b/crates/uv-distribution/src/workspace.rs index 3ae48ecfc..46f8a4a63 100644 --- a/crates/uv-distribution/src/workspace.rs +++ b/crates/uv-distribution/src/workspace.rs @@ -13,7 +13,7 @@ use uv_fs::{absolutize_path, Simplified}; use uv_normalize::PackageName; use uv_warnings::warn_user; -use crate::pyproject::{PyProjectToml, Source, ToolUvWorkspace}; +use crate::pyproject::{Project, PyProjectToml, Source, ToolUvWorkspace}; #[derive(thiserror::Error, Debug)] pub enum WorkspaceError { @@ -118,7 +118,11 @@ impl Workspace { let current_project = pyproject_toml .project .clone() - .map(|project| (project.name.clone(), project_path, pyproject_toml)); + .map(|project| WorkspaceMember { + root: project_path, + project, + pyproject_toml, + }); Self::collect_members( workspace_root, workspace_definition, @@ -162,7 +166,7 @@ impl Workspace { workspace_root: PathBuf, workspace_definition: ToolUvWorkspace, workspace_pyproject_toml: PyProjectToml, - current_project: Option<(PackageName, PathBuf, PyProjectToml)>, + current_project: Option, stop_discovery_at: Option<&Path>, ) -> Result { let mut workspace_members = BTreeMap::new(); @@ -173,7 +177,7 @@ impl Workspace { // project. if current_project .as_ref() - .map(|(_, path, _)| path != &workspace_root) + .map(|root_member| root_member.root != workspace_root) .unwrap_or(true) { if let Some(project) = &workspace_pyproject_toml.project { @@ -192,6 +196,7 @@ impl Workspace { project.name.clone(), WorkspaceMember { root: workspace_root.clone(), + project: project.clone(), pyproject_toml, }, ); @@ -199,20 +204,14 @@ impl Workspace { } // The current project is a workspace member, especially in a single project workspace. - if let Some((project_name, project_path, project)) = current_project { + if let Some(root_member) = current_project { debug!( "Adding current workspace member: {}", - project_path.simplified_display() + root_member.root.simplified_display() ); - seen.insert(project_path.clone()); - workspace_members.insert( - project_name, - WorkspaceMember { - root: project_path.clone(), - pyproject_toml: project.clone(), - }, - ); + seen.insert(root_member.root.clone()); + workspace_members.insert(root_member.project.name.clone(), root_member); } // Add all other workspace members. @@ -247,16 +246,18 @@ impl Workspace { return Err(WorkspaceError::MissingProject(member_root)); }; - let member = WorkspaceMember { - root: member_root.clone(), - pyproject_toml, - }; - debug!( "Adding discovered workspace member: {}", member_root.simplified_display() ); - workspace_members.insert(project.name, member); + workspace_members.insert( + project.name.clone(), + WorkspaceMember { + root: member_root.clone(), + project, + pyproject_toml, + }, + ); } } let workspace_sources = workspace_pyproject_toml @@ -281,6 +282,9 @@ impl Workspace { pub struct WorkspaceMember { /// The path to the project root. root: PathBuf, + /// The `[project]` table, from the `pyproject.toml` of the project found at + /// `/pyproject.toml`. + project: Project, /// The `pyproject.toml` of the project, found at `/pyproject.toml`. pyproject_toml: PyProjectToml, } @@ -291,6 +295,12 @@ impl WorkspaceMember { &self.root } + /// The `[project]` table, from the `pyproject.toml` of the project found at + /// `/pyproject.toml`. + pub fn project(&self) -> &Project { + &self.project + } + /// The `pyproject.toml` of the project, found at `/pyproject.toml`. pub fn pyproject_toml(&self) -> &PyProjectToml { &self.pyproject_toml @@ -430,13 +440,7 @@ impl ProjectWorkspace { .clone() .ok_or_else(|| WorkspaceError::MissingProject(pyproject_path.clone()))?; - Self::from_project( - project_root, - &pyproject_toml, - project.name, - stop_discovery_at, - ) - .await + Self::from_project(project_root, &project, &pyproject_toml, stop_discovery_at).await } /// If the current directory contains a `pyproject.toml` with a `project` table, discover the @@ -461,13 +465,7 @@ impl ProjectWorkspace { }; Ok(Some( - Self::from_project( - project_root, - &pyproject_toml, - project.name, - stop_discovery_at, - ) - .await?, + Self::from_project(project_root, &project, &pyproject_toml, stop_discovery_at).await?, )) } @@ -519,8 +517,8 @@ impl ProjectWorkspace { /// Find the workspace for a project. pub async fn from_project( project_path: &Path, - project: &PyProjectToml, - project_name: PackageName, + project: &Project, + project_pyproject_toml: &PyProjectToml, stop_discovery_at: Option<&Path>, ) -> Result { let project_path = absolutize_path(project_path) @@ -528,12 +526,18 @@ impl ProjectWorkspace { .to_path_buf(); // Check if the current project is also an explicit workspace root. - let mut workspace = project + let mut workspace = project_pyproject_toml .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.workspace.as_ref()) - .map(|workspace| (project_path.clone(), workspace.clone(), project.clone())); + .map(|workspace| { + ( + project_path.clone(), + workspace.clone(), + project_pyproject_toml.clone(), + ) + }); if workspace.is_none() { // The project isn't an explicit workspace root, check if we're a regular workspace @@ -541,21 +545,23 @@ impl ProjectWorkspace { workspace = find_workspace(&project_path, stop_discovery_at).await?; } + let current_project = WorkspaceMember { + root: project_path.clone(), + project: project.clone(), + pyproject_toml: project_pyproject_toml.clone(), + }; + let Some((workspace_root, workspace_definition, workspace_pyproject_toml)) = workspace else { // The project isn't an explicit workspace root, but there's also no workspace root // above it, so the project is an implicit workspace root identical to the project root. debug!("No workspace root found, using project root"); - let current_project_as_members = BTreeMap::from_iter([( - project_name.clone(), - WorkspaceMember { - root: project_path.clone(), - pyproject_toml: project.clone(), - }, - )]); + + let current_project_as_members = + BTreeMap::from_iter([(project.name.clone(), current_project)]); return Ok(Self { project_root: project_path.clone(), - project_name: project_name.clone(), + project_name: project.name.clone(), workspace: Workspace { root: project_path.clone(), packages: current_project_as_members, @@ -575,14 +581,14 @@ impl ProjectWorkspace { workspace_root, workspace_definition, workspace_pyproject_toml, - Some((project_name.clone(), project_path.clone(), project.clone())), + Some(current_project), stop_discovery_at, ) .await?; Ok(Self { project_root: project_path, - project_name, + project_name: project.name.clone(), workspace, }) } @@ -818,6 +824,11 @@ mod tests { "packages": { "bird-feeder": { "root": "[ROOT]/albatross-in-example/examples/bird-feeder", + "project": { + "name": "bird-feeder", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" } }, @@ -848,6 +859,11 @@ mod tests { "packages": { "bird-feeder": { "root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", + "project": { + "name": "bird-feeder", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" } }, @@ -877,14 +893,29 @@ mod tests { "packages": { "albatross": { "root": "[ROOT]/albatross-root-workspace", + "project": { + "name": "albatross", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" }, "bird-feeder": { "root": "[ROOT]/albatross-root-workspace/packages/bird-feeder", + "project": { + "name": "bird-feeder", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" }, "seeds": { "root": "[ROOT]/albatross-root-workspace/packages/seeds", + "project": { + "name": "seeds", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" } }, @@ -920,14 +951,29 @@ mod tests { "packages": { "albatross": { "root": "[ROOT]/albatross-virtual-workspace/packages/albatross", + "project": { + "name": "albatross", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" }, "bird-feeder": { "root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder", + "project": { + "name": "bird-feeder", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" }, "seeds": { "root": "[ROOT]/albatross-virtual-workspace/packages/seeds", + "project": { + "name": "seeds", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" } }, @@ -957,6 +1003,11 @@ mod tests { "packages": { "albatross": { "root": "[ROOT]/albatross-just-project", + "project": { + "name": "albatross", + "requires-python": null, + "optional-dependencies": null + }, "pyproject_toml": "[PYPROJECT_TOML]" } }, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 2ed2c7147..33a1dde14 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -95,12 +95,7 @@ pub(super) async fn do_lock( let interpreter = venv.interpreter(); let tags = venv.interpreter().tags()?; let markers = venv.interpreter().markers(); - let requires_python = project - .current_project() - .pyproject_toml() - .project - .as_ref() - .and_then(|project| project.requires_python.as_ref()); + let requires_python = project.current_project().project().requires_python.as_ref(); // Initialize the registry client. // TODO(zanieb): Support client options e.g. offline, tls, etc.