mirror of https://github.com/astral-sh/uv
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:
parent
ae9610104a
commit
b05a39c735
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue