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)]
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<Metadata> {
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,

View File

@ -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<WorkspaceMember>,
stop_discovery_at: Option<&Path>,
) -> Result<Workspace, WorkspaceError> {
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
/// `<root>/pyproject.toml`.
project: Project,
/// The `pyproject.toml` of the project, found at `<root>/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
/// `<root>/pyproject.toml`.
pub fn project(&self) -> &Project {
&self.project
}
/// The `pyproject.toml` of the project, found at `<root>/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<Self, WorkspaceError> {
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]"
}
},

View File

@ -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.