Skip to content

Commit b05a39c

Browse files
authored
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.
1 parent ae96101 commit b05a39c

File tree

3 files changed

+111
-59
lines changed

3 files changed

+111
-59
lines changed

crates/uv-distribution/src/requirement_lowering.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,14 @@ fn path_source(
242242

243243
#[cfg(test)]
244244
mod test {
245+
use anyhow::Context;
245246
use std::path::Path;
246-
use std::str::FromStr;
247247

248248
use indoc::indoc;
249249
use insta::assert_snapshot;
250250

251251
use pypi_types::Metadata23;
252252
use uv_configuration::PreviewMode;
253-
use uv_normalize::PackageName;
254253

255254
use crate::metadata::Metadata;
256255
use crate::pyproject::PyProjectToml;
@@ -259,9 +258,16 @@ mod test {
259258
async fn metadata_from_pyproject_toml(contents: &str) -> anyhow::Result<Metadata> {
260259
let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
261260
let path = Path::new("pyproject.toml");
262-
let project_name = PackageName::from_str("foo").unwrap();
263-
let project_workspace =
264-
ProjectWorkspace::from_project(path, &pyproject_toml, project_name, Some(path)).await?;
261+
let project_workspace = ProjectWorkspace::from_project(
262+
path,
263+
pyproject_toml
264+
.project
265+
.as_ref()
266+
.context("metadata field project not found")?,
267+
&pyproject_toml,
268+
Some(path),
269+
)
270+
.await?;
265271
let metadata = Metadata23::parse_pyproject_toml(contents)?;
266272
Ok(Metadata::from_project_workspace(
267273
metadata,

crates/uv-distribution/src/workspace.rs

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use uv_fs::{absolutize_path, Simplified};
1313
use uv_normalize::PackageName;
1414
use uv_warnings::warn_user;
1515

16-
use crate::pyproject::{PyProjectToml, Source, ToolUvWorkspace};
16+
use crate::pyproject::{Project, PyProjectToml, Source, ToolUvWorkspace};
1717

1818
#[derive(thiserror::Error, Debug)]
1919
pub enum WorkspaceError {
@@ -118,7 +118,11 @@ impl Workspace {
118118
let current_project = pyproject_toml
119119
.project
120120
.clone()
121-
.map(|project| (project.name.clone(), project_path, pyproject_toml));
121+
.map(|project| WorkspaceMember {
122+
root: project_path,
123+
project,
124+
pyproject_toml,
125+
});
122126
Self::collect_members(
123127
workspace_root,
124128
workspace_definition,
@@ -162,7 +166,7 @@ impl Workspace {
162166
workspace_root: PathBuf,
163167
workspace_definition: ToolUvWorkspace,
164168
workspace_pyproject_toml: PyProjectToml,
165-
current_project: Option<(PackageName, PathBuf, PyProjectToml)>,
169+
current_project: Option<WorkspaceMember>,
166170
stop_discovery_at: Option<&Path>,
167171
) -> Result<Workspace, WorkspaceError> {
168172
let mut workspace_members = BTreeMap::new();
@@ -173,7 +177,7 @@ impl Workspace {
173177
// project.
174178
if current_project
175179
.as_ref()
176-
.map(|(_, path, _)| path != &workspace_root)
180+
.map(|root_member| root_member.root != workspace_root)
177181
.unwrap_or(true)
178182
{
179183
if let Some(project) = &workspace_pyproject_toml.project {
@@ -192,27 +196,22 @@ impl Workspace {
192196
project.name.clone(),
193197
WorkspaceMember {
194198
root: workspace_root.clone(),
199+
project: project.clone(),
195200
pyproject_toml,
196201
},
197202
);
198203
};
199204
}
200205

201206
// The current project is a workspace member, especially in a single project workspace.
202-
if let Some((project_name, project_path, project)) = current_project {
207+
if let Some(root_member) = current_project {
203208
debug!(
204209
"Adding current workspace member: {}",
205-
project_path.simplified_display()
210+
root_member.root.simplified_display()
206211
);
207212

208-
seen.insert(project_path.clone());
209-
workspace_members.insert(
210-
project_name,
211-
WorkspaceMember {
212-
root: project_path.clone(),
213-
pyproject_toml: project.clone(),
214-
},
215-
);
213+
seen.insert(root_member.root.clone());
214+
workspace_members.insert(root_member.project.name.clone(), root_member);
216215
}
217216

218217
// Add all other workspace members.
@@ -247,16 +246,18 @@ impl Workspace {
247246
return Err(WorkspaceError::MissingProject(member_root));
248247
};
249248

250-
let member = WorkspaceMember {
251-
root: member_root.clone(),
252-
pyproject_toml,
253-
};
254-
255249
debug!(
256250
"Adding discovered workspace member: {}",
257251
member_root.simplified_display()
258252
);
259-
workspace_members.insert(project.name, member);
253+
workspace_members.insert(
254+
project.name.clone(),
255+
WorkspaceMember {
256+
root: member_root.clone(),
257+
project,
258+
pyproject_toml,
259+
},
260+
);
260261
}
261262
}
262263
let workspace_sources = workspace_pyproject_toml
@@ -281,6 +282,9 @@ impl Workspace {
281282
pub struct WorkspaceMember {
282283
/// The path to the project root.
283284
root: PathBuf,
285+
/// The `[project]` table, from the `pyproject.toml` of the project found at
286+
/// `<root>/pyproject.toml`.
287+
project: Project,
284288
/// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`.
285289
pyproject_toml: PyProjectToml,
286290
}
@@ -291,6 +295,12 @@ impl WorkspaceMember {
291295
&self.root
292296
}
293297

298+
/// The `[project]` table, from the `pyproject.toml` of the project found at
299+
/// `<root>/pyproject.toml`.
300+
pub fn project(&self) -> &Project {
301+
&self.project
302+
}
303+
294304
/// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`.
295305
pub fn pyproject_toml(&self) -> &PyProjectToml {
296306
&self.pyproject_toml
@@ -430,13 +440,7 @@ impl ProjectWorkspace {
430440
.clone()
431441
.ok_or_else(|| WorkspaceError::MissingProject(pyproject_path.clone()))?;
432442

433-
Self::from_project(
434-
project_root,
435-
&pyproject_toml,
436-
project.name,
437-
stop_discovery_at,
438-
)
439-
.await
443+
Self::from_project(project_root, &project, &pyproject_toml, stop_discovery_at).await
440444
}
441445

442446
/// If the current directory contains a `pyproject.toml` with a `project` table, discover the
@@ -461,13 +465,7 @@ impl ProjectWorkspace {
461465
};
462466

463467
Ok(Some(
464-
Self::from_project(
465-
project_root,
466-
&pyproject_toml,
467-
project.name,
468-
stop_discovery_at,
469-
)
470-
.await?,
468+
Self::from_project(project_root, &project, &pyproject_toml, stop_discovery_at).await?,
471469
))
472470
}
473471

@@ -519,43 +517,51 @@ impl ProjectWorkspace {
519517
/// Find the workspace for a project.
520518
pub async fn from_project(
521519
project_path: &Path,
522-
project: &PyProjectToml,
523-
project_name: PackageName,
520+
project: &Project,
521+
project_pyproject_toml: &PyProjectToml,
524522
stop_discovery_at: Option<&Path>,
525523
) -> Result<Self, WorkspaceError> {
526524
let project_path = absolutize_path(project_path)
527525
.map_err(WorkspaceError::Normalize)?
528526
.to_path_buf();
529527

530528
// Check if the current project is also an explicit workspace root.
531-
let mut workspace = project
529+
let mut workspace = project_pyproject_toml
532530
.tool
533531
.as_ref()
534532
.and_then(|tool| tool.uv.as_ref())
535533
.and_then(|uv| uv.workspace.as_ref())
536-
.map(|workspace| (project_path.clone(), workspace.clone(), project.clone()));
534+
.map(|workspace| {
535+
(
536+
project_path.clone(),
537+
workspace.clone(),
538+
project_pyproject_toml.clone(),
539+
)
540+
});
537541

538542
if workspace.is_none() {
539543
// The project isn't an explicit workspace root, check if we're a regular workspace
540544
// member by looking for an explicit workspace root above.
541545
workspace = find_workspace(&project_path, stop_discovery_at).await?;
542546
}
543547

548+
let current_project = WorkspaceMember {
549+
root: project_path.clone(),
550+
project: project.clone(),
551+
pyproject_toml: project_pyproject_toml.clone(),
552+
};
553+
544554
let Some((workspace_root, workspace_definition, workspace_pyproject_toml)) = workspace
545555
else {
546556
// The project isn't an explicit workspace root, but there's also no workspace root
547557
// above it, so the project is an implicit workspace root identical to the project root.
548558
debug!("No workspace root found, using project root");
549-
let current_project_as_members = BTreeMap::from_iter([(
550-
project_name.clone(),
551-
WorkspaceMember {
552-
root: project_path.clone(),
553-
pyproject_toml: project.clone(),
554-
},
555-
)]);
559+
560+
let current_project_as_members =
561+
BTreeMap::from_iter([(project.name.clone(), current_project)]);
556562
return Ok(Self {
557563
project_root: project_path.clone(),
558-
project_name: project_name.clone(),
564+
project_name: project.name.clone(),
559565
workspace: Workspace {
560566
root: project_path.clone(),
561567
packages: current_project_as_members,
@@ -575,14 +581,14 @@ impl ProjectWorkspace {
575581
workspace_root,
576582
workspace_definition,
577583
workspace_pyproject_toml,
578-
Some((project_name.clone(), project_path.clone(), project.clone())),
584+
Some(current_project),
579585
stop_discovery_at,
580586
)
581587
.await?;
582588

583589
Ok(Self {
584590
project_root: project_path,
585-
project_name,
591+
project_name: project.name.clone(),
586592
workspace,
587593
})
588594
}
@@ -818,6 +824,11 @@ mod tests {
818824
"packages": {
819825
"bird-feeder": {
820826
"root": "[ROOT]/albatross-in-example/examples/bird-feeder",
827+
"project": {
828+
"name": "bird-feeder",
829+
"requires-python": null,
830+
"optional-dependencies": null
831+
},
821832
"pyproject_toml": "[PYPROJECT_TOML]"
822833
}
823834
},
@@ -848,6 +859,11 @@ mod tests {
848859
"packages": {
849860
"bird-feeder": {
850861
"root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
862+
"project": {
863+
"name": "bird-feeder",
864+
"requires-python": null,
865+
"optional-dependencies": null
866+
},
851867
"pyproject_toml": "[PYPROJECT_TOML]"
852868
}
853869
},
@@ -877,14 +893,29 @@ mod tests {
877893
"packages": {
878894
"albatross": {
879895
"root": "[ROOT]/albatross-root-workspace",
896+
"project": {
897+
"name": "albatross",
898+
"requires-python": null,
899+
"optional-dependencies": null
900+
},
880901
"pyproject_toml": "[PYPROJECT_TOML]"
881902
},
882903
"bird-feeder": {
883904
"root": "[ROOT]/albatross-root-workspace/packages/bird-feeder",
905+
"project": {
906+
"name": "bird-feeder",
907+
"requires-python": null,
908+
"optional-dependencies": null
909+
},
884910
"pyproject_toml": "[PYPROJECT_TOML]"
885911
},
886912
"seeds": {
887913
"root": "[ROOT]/albatross-root-workspace/packages/seeds",
914+
"project": {
915+
"name": "seeds",
916+
"requires-python": null,
917+
"optional-dependencies": null
918+
},
888919
"pyproject_toml": "[PYPROJECT_TOML]"
889920
}
890921
},
@@ -920,14 +951,29 @@ mod tests {
920951
"packages": {
921952
"albatross": {
922953
"root": "[ROOT]/albatross-virtual-workspace/packages/albatross",
954+
"project": {
955+
"name": "albatross",
956+
"requires-python": null,
957+
"optional-dependencies": null
958+
},
923959
"pyproject_toml": "[PYPROJECT_TOML]"
924960
},
925961
"bird-feeder": {
926962
"root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder",
963+
"project": {
964+
"name": "bird-feeder",
965+
"requires-python": null,
966+
"optional-dependencies": null
967+
},
927968
"pyproject_toml": "[PYPROJECT_TOML]"
928969
},
929970
"seeds": {
930971
"root": "[ROOT]/albatross-virtual-workspace/packages/seeds",
972+
"project": {
973+
"name": "seeds",
974+
"requires-python": null,
975+
"optional-dependencies": null
976+
},
931977
"pyproject_toml": "[PYPROJECT_TOML]"
932978
}
933979
},
@@ -957,6 +1003,11 @@ mod tests {
9571003
"packages": {
9581004
"albatross": {
9591005
"root": "[ROOT]/albatross-just-project",
1006+
"project": {
1007+
"name": "albatross",
1008+
"requires-python": null,
1009+
"optional-dependencies": null
1010+
},
9601011
"pyproject_toml": "[PYPROJECT_TOML]"
9611012
}
9621013
},

crates/uv/src/commands/project/lock.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,7 @@ pub(super) async fn do_lock(
9595
let interpreter = venv.interpreter();
9696
let tags = venv.interpreter().tags()?;
9797
let markers = venv.interpreter().markers();
98-
let requires_python = project
99-
.current_project()
100-
.pyproject_toml()
101-
.project
102-
.as_ref()
103-
.and_then(|project| project.requires_python.as_ref());
98+
let requires_python = project.current_project().project().requires_python.as_ref();
10499

105100
// Initialize the registry client.
106101
// TODO(zanieb): Support client options e.g. offline, tls, etc.

0 commit comments

Comments
 (0)