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.
This commit is contained in:
konsti 2024-06-05 18:48:19 +02:00 committed by GitHub
parent ae9610104a
commit b05a39c735
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 59 deletions

View File

@ -242,15 +242,14 @@ fn path_source(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use anyhow::Context;
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use indoc::indoc; use indoc::indoc;
use insta::assert_snapshot; use insta::assert_snapshot;
use pypi_types::Metadata23; use pypi_types::Metadata23;
use uv_configuration::PreviewMode; use uv_configuration::PreviewMode;
use uv_normalize::PackageName;
use crate::metadata::Metadata; use crate::metadata::Metadata;
use crate::pyproject::PyProjectToml; use crate::pyproject::PyProjectToml;
@ -259,9 +258,16 @@ mod test {
async fn metadata_from_pyproject_toml(contents: &str) -> anyhow::Result<Metadata> { async fn metadata_from_pyproject_toml(contents: &str) -> anyhow::Result<Metadata> {
let pyproject_toml: PyProjectToml = toml::from_str(contents)?; let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
let path = Path::new("pyproject.toml"); let path = Path::new("pyproject.toml");
let project_name = PackageName::from_str("foo").unwrap(); let project_workspace = ProjectWorkspace::from_project(
let project_workspace = path,
ProjectWorkspace::from_project(path, &pyproject_toml, project_name, Some(path)).await?; pyproject_toml
.project
.as_ref()
.context("metadata field project not found")?,
&pyproject_toml,
Some(path),
)
.await?;
let metadata = Metadata23::parse_pyproject_toml(contents)?; let metadata = Metadata23::parse_pyproject_toml(contents)?;
Ok(Metadata::from_project_workspace( Ok(Metadata::from_project_workspace(
metadata, metadata,

View File

@ -13,7 +13,7 @@ use uv_fs::{absolutize_path, Simplified};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_warnings::warn_user; use uv_warnings::warn_user;
use crate::pyproject::{PyProjectToml, Source, ToolUvWorkspace}; use crate::pyproject::{Project, PyProjectToml, Source, ToolUvWorkspace};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum WorkspaceError { pub enum WorkspaceError {
@ -118,7 +118,11 @@ impl Workspace {
let current_project = pyproject_toml let current_project = pyproject_toml
.project .project
.clone() .clone()
.map(|project| (project.name.clone(), project_path, pyproject_toml)); .map(|project| WorkspaceMember {
root: project_path,
project,
pyproject_toml,
});
Self::collect_members( Self::collect_members(
workspace_root, workspace_root,
workspace_definition, workspace_definition,
@ -162,7 +166,7 @@ impl Workspace {
workspace_root: PathBuf, workspace_root: PathBuf,
workspace_definition: ToolUvWorkspace, workspace_definition: ToolUvWorkspace,
workspace_pyproject_toml: PyProjectToml, workspace_pyproject_toml: PyProjectToml,
current_project: Option<(PackageName, PathBuf, PyProjectToml)>, current_project: Option<WorkspaceMember>,
stop_discovery_at: Option<&Path>, stop_discovery_at: Option<&Path>,
) -> Result<Workspace, WorkspaceError> { ) -> Result<Workspace, WorkspaceError> {
let mut workspace_members = BTreeMap::new(); let mut workspace_members = BTreeMap::new();
@ -173,7 +177,7 @@ impl Workspace {
// project. // project.
if current_project if current_project
.as_ref() .as_ref()
.map(|(_, path, _)| path != &workspace_root) .map(|root_member| root_member.root != workspace_root)
.unwrap_or(true) .unwrap_or(true)
{ {
if let Some(project) = &workspace_pyproject_toml.project { if let Some(project) = &workspace_pyproject_toml.project {
@ -192,6 +196,7 @@ impl Workspace {
project.name.clone(), project.name.clone(),
WorkspaceMember { WorkspaceMember {
root: workspace_root.clone(), root: workspace_root.clone(),
project: project.clone(),
pyproject_toml, pyproject_toml,
}, },
); );
@ -199,20 +204,14 @@ impl Workspace {
} }
// The current project is a workspace member, especially in a single project 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!( debug!(
"Adding current workspace member: {}", "Adding current workspace member: {}",
project_path.simplified_display() root_member.root.simplified_display()
); );
seen.insert(project_path.clone()); seen.insert(root_member.root.clone());
workspace_members.insert( workspace_members.insert(root_member.project.name.clone(), root_member);
project_name,
WorkspaceMember {
root: project_path.clone(),
pyproject_toml: project.clone(),
},
);
} }
// Add all other workspace members. // Add all other workspace members.
@ -247,16 +246,18 @@ impl Workspace {
return Err(WorkspaceError::MissingProject(member_root)); return Err(WorkspaceError::MissingProject(member_root));
}; };
let member = WorkspaceMember {
root: member_root.clone(),
pyproject_toml,
};
debug!( debug!(
"Adding discovered workspace member: {}", "Adding discovered workspace member: {}",
member_root.simplified_display() 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 let workspace_sources = workspace_pyproject_toml
@ -281,6 +282,9 @@ impl Workspace {
pub struct WorkspaceMember { pub struct WorkspaceMember {
/// The path to the project root. /// The path to the project root.
root: PathBuf, root: PathBuf,
/// The `[project]` table, from the `pyproject.toml` of the project found at
/// `<root>/pyproject.toml`.
project: Project,
/// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`. /// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`.
pyproject_toml: PyProjectToml, pyproject_toml: PyProjectToml,
} }
@ -291,6 +295,12 @@ impl WorkspaceMember {
&self.root &self.root
} }
/// The `[project]` table, from the `pyproject.toml` of the project found at
/// `<root>/pyproject.toml`.
pub fn project(&self) -> &Project {
&self.project
}
/// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`. /// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`.
pub fn pyproject_toml(&self) -> &PyProjectToml { pub fn pyproject_toml(&self) -> &PyProjectToml {
&self.pyproject_toml &self.pyproject_toml
@ -430,13 +440,7 @@ impl ProjectWorkspace {
.clone() .clone()
.ok_or_else(|| WorkspaceError::MissingProject(pyproject_path.clone()))?; .ok_or_else(|| WorkspaceError::MissingProject(pyproject_path.clone()))?;
Self::from_project( Self::from_project(project_root, &project, &pyproject_toml, stop_discovery_at).await
project_root,
&pyproject_toml,
project.name,
stop_discovery_at,
)
.await
} }
/// If the current directory contains a `pyproject.toml` with a `project` table, discover the /// If the current directory contains a `pyproject.toml` with a `project` table, discover the
@ -461,13 +465,7 @@ impl ProjectWorkspace {
}; };
Ok(Some( Ok(Some(
Self::from_project( Self::from_project(project_root, &project, &pyproject_toml, stop_discovery_at).await?,
project_root,
&pyproject_toml,
project.name,
stop_discovery_at,
)
.await?,
)) ))
} }
@ -519,8 +517,8 @@ impl ProjectWorkspace {
/// Find the workspace for a project. /// Find the workspace for a project.
pub async fn from_project( pub async fn from_project(
project_path: &Path, project_path: &Path,
project: &PyProjectToml, project: &Project,
project_name: PackageName, project_pyproject_toml: &PyProjectToml,
stop_discovery_at: Option<&Path>, stop_discovery_at: Option<&Path>,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
let project_path = absolutize_path(project_path) let project_path = absolutize_path(project_path)
@ -528,12 +526,18 @@ impl ProjectWorkspace {
.to_path_buf(); .to_path_buf();
// Check if the current project is also an explicit workspace root. // Check if the current project is also an explicit workspace root.
let mut workspace = project let mut workspace = project_pyproject_toml
.tool .tool
.as_ref() .as_ref()
.and_then(|tool| tool.uv.as_ref()) .and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.workspace.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() { if workspace.is_none() {
// The project isn't an explicit workspace root, check if we're a regular workspace // 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?; 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 let Some((workspace_root, workspace_definition, workspace_pyproject_toml)) = workspace
else { else {
// The project isn't an explicit workspace root, but there's also no workspace root // 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. // above it, so the project is an implicit workspace root identical to the project root.
debug!("No workspace root found, using project root"); debug!("No workspace root found, using project root");
let current_project_as_members = BTreeMap::from_iter([(
project_name.clone(), let current_project_as_members =
WorkspaceMember { BTreeMap::from_iter([(project.name.clone(), current_project)]);
root: project_path.clone(),
pyproject_toml: project.clone(),
},
)]);
return Ok(Self { return Ok(Self {
project_root: project_path.clone(), project_root: project_path.clone(),
project_name: project_name.clone(), project_name: project.name.clone(),
workspace: Workspace { workspace: Workspace {
root: project_path.clone(), root: project_path.clone(),
packages: current_project_as_members, packages: current_project_as_members,
@ -575,14 +581,14 @@ impl ProjectWorkspace {
workspace_root, workspace_root,
workspace_definition, workspace_definition,
workspace_pyproject_toml, workspace_pyproject_toml,
Some((project_name.clone(), project_path.clone(), project.clone())), Some(current_project),
stop_discovery_at, stop_discovery_at,
) )
.await?; .await?;
Ok(Self { Ok(Self {
project_root: project_path, project_root: project_path,
project_name, project_name: project.name.clone(),
workspace, workspace,
}) })
} }
@ -818,6 +824,11 @@ mod tests {
"packages": { "packages": {
"bird-feeder": { "bird-feeder": {
"root": "[ROOT]/albatross-in-example/examples/bird-feeder", "root": "[ROOT]/albatross-in-example/examples/bird-feeder",
"project": {
"name": "bird-feeder",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
} }
}, },
@ -848,6 +859,11 @@ mod tests {
"packages": { "packages": {
"bird-feeder": { "bird-feeder": {
"root": "[ROOT]/albatross-project-in-excluded/excluded/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]" "pyproject_toml": "[PYPROJECT_TOML]"
} }
}, },
@ -877,14 +893,29 @@ mod tests {
"packages": { "packages": {
"albatross": { "albatross": {
"root": "[ROOT]/albatross-root-workspace", "root": "[ROOT]/albatross-root-workspace",
"project": {
"name": "albatross",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
}, },
"bird-feeder": { "bird-feeder": {
"root": "[ROOT]/albatross-root-workspace/packages/bird-feeder", "root": "[ROOT]/albatross-root-workspace/packages/bird-feeder",
"project": {
"name": "bird-feeder",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
}, },
"seeds": { "seeds": {
"root": "[ROOT]/albatross-root-workspace/packages/seeds", "root": "[ROOT]/albatross-root-workspace/packages/seeds",
"project": {
"name": "seeds",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
} }
}, },
@ -920,14 +951,29 @@ mod tests {
"packages": { "packages": {
"albatross": { "albatross": {
"root": "[ROOT]/albatross-virtual-workspace/packages/albatross", "root": "[ROOT]/albatross-virtual-workspace/packages/albatross",
"project": {
"name": "albatross",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
}, },
"bird-feeder": { "bird-feeder": {
"root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder", "root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder",
"project": {
"name": "bird-feeder",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
}, },
"seeds": { "seeds": {
"root": "[ROOT]/albatross-virtual-workspace/packages/seeds", "root": "[ROOT]/albatross-virtual-workspace/packages/seeds",
"project": {
"name": "seeds",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
} }
}, },
@ -957,6 +1003,11 @@ mod tests {
"packages": { "packages": {
"albatross": { "albatross": {
"root": "[ROOT]/albatross-just-project", "root": "[ROOT]/albatross-just-project",
"project": {
"name": "albatross",
"requires-python": null,
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]" "pyproject_toml": "[PYPROJECT_TOML]"
} }
}, },

View File

@ -95,12 +95,7 @@ pub(super) async fn do_lock(
let interpreter = venv.interpreter(); let interpreter = venv.interpreter();
let tags = venv.interpreter().tags()?; let tags = venv.interpreter().tags()?;
let markers = venv.interpreter().markers(); let markers = venv.interpreter().markers();
let requires_python = project let requires_python = project.current_project().project().requires_python.as_ref();
.current_project()
.pyproject_toml()
.project
.as_ref()
.and_then(|project| project.requires_python.as_ref());
// Initialize the registry client. // Initialize the registry client.
// TODO(zanieb): Support client options e.g. offline, tls, etc. // TODO(zanieb): Support client options e.g. offline, tls, etc.