@@ -20,7 +20,7 @@ use uv_warnings::warn_user_once;
2020
2121use crate :: dependency_groups:: { DependencyGroupError , FlatDependencyGroup , FlatDependencyGroups } ;
2222use crate :: pyproject:: {
23- Project , PyProjectToml , PyprojectTomlError , Sources , ToolUvSources , ToolUvWorkspace ,
23+ Project , PyProjectToml , PyprojectTomlError , Source , Sources , ToolUvSources , ToolUvWorkspace ,
2424} ;
2525
2626type WorkspaceMembers = Arc < BTreeMap < PackageName , WorkspaceMember > > ;
@@ -109,6 +109,8 @@ pub struct Workspace {
109109 install_path : PathBuf ,
110110 /// The members of the workspace.
111111 packages : WorkspaceMembers ,
112+ /// The workspace members that are required by other members.
113+ required_members : BTreeSet < PackageName > ,
112114 /// The sources table from the workspace `pyproject.toml`.
113115 ///
114116 /// This table is overridden by the project sources.
@@ -260,6 +262,7 @@ impl Workspace {
260262 pyproject_toml : PyProjectToml ,
261263 ) -> Option < Self > {
262264 let mut packages = self . packages ;
265+
263266 let member = Arc :: make_mut ( & mut packages) . get_mut ( package_name) ?;
264267
265268 if member. root == self . install_path {
@@ -279,17 +282,33 @@ impl Workspace {
279282 // Set the `pyproject.toml` for the member.
280283 member. pyproject_toml = pyproject_toml;
281284
285+ // Recompute required_members with the updated data
286+ let required_members = Self :: collect_required_members (
287+ & packages,
288+ & workspace_sources,
289+ & workspace_pyproject_toml,
290+ ) ;
291+
282292 Some ( Self {
283293 pyproject_toml : workspace_pyproject_toml,
284294 sources : workspace_sources,
285295 packages,
296+ required_members,
286297 ..self
287298 } )
288299 } else {
289300 // Set the `pyproject.toml` for the member.
290301 member. pyproject_toml = pyproject_toml;
291302
292- Some ( Self { packages, ..self } )
303+ // Recompute required_members with the updated member data
304+ let required_members =
305+ Self :: collect_required_members ( & packages, & self . sources , & self . pyproject_toml ) ;
306+
307+ Some ( Self {
308+ packages,
309+ required_members,
310+ ..self
311+ } )
293312 }
294313 }
295314
@@ -303,7 +322,7 @@ impl Workspace {
303322
304323 /// Returns the set of all workspace members.
305324 pub fn members_requirements ( & self ) -> impl Iterator < Item = Requirement > + ' _ {
306- self . packages . values ( ) . filter_map ( |member| {
325+ self . packages . iter ( ) . filter_map ( |( name , member) | {
307326 let url = VerbatimUrl :: from_absolute_path ( & member. root )
308327 . expect ( "path is valid URL" )
309328 . with_given ( member. root . to_string_lossy ( ) ) ;
@@ -312,7 +331,10 @@ impl Workspace {
312331 extras : Box :: new ( [ ] ) ,
313332 groups : Box :: new ( [ ] ) ,
314333 marker : MarkerTree :: TRUE ,
315- source : if member. pyproject_toml . is_package ( ) {
334+ source : if member
335+ . pyproject_toml ( )
336+ . is_package ( !self . is_required_member ( name) )
337+ {
316338 RequirementSource :: Directory {
317339 install_path : member. root . clone ( ) . into_boxed_path ( ) ,
318340 editable : Some ( true ) ,
@@ -332,9 +354,65 @@ impl Workspace {
332354 } )
333355 }
334356
357+ /// The workspace members that are required my another member of the workspace.
358+ pub fn required_members ( & self ) -> & BTreeSet < PackageName > {
359+ & self . required_members
360+ }
361+
362+ /// Compute the workspace members that are required by another member of the workspace.
363+ ///
364+ /// N.B. this checks if a workspace member is required by inspecting `tool.uv.source` entries,
365+ /// but does not actually check if the source is _used_, which could result in false positives
366+ /// but is easier to compute.
367+ fn collect_required_members (
368+ packages : & BTreeMap < PackageName , WorkspaceMember > ,
369+ sources : & BTreeMap < PackageName , Sources > ,
370+ pyproject_toml : & PyProjectToml ,
371+ ) -> BTreeSet < PackageName > {
372+ sources
373+ . iter ( )
374+ . filter ( |( name, _) | {
375+ pyproject_toml
376+ . project
377+ . as_ref ( )
378+ . is_none_or ( |project| project. name != * * name)
379+ } )
380+ . chain (
381+ packages
382+ . iter ( )
383+ . filter_map ( |( name, member) | {
384+ member
385+ . pyproject_toml
386+ . tool
387+ . as_ref ( )
388+ . and_then ( |tool| tool. uv . as_ref ( ) )
389+ . and_then ( |uv| uv. sources . as_ref ( ) )
390+ . map ( ToolUvSources :: inner)
391+ . map ( move |sources| {
392+ sources
393+ . iter ( )
394+ . filter ( move |( source_name, _) | name != * source_name)
395+ } )
396+ } )
397+ . flatten ( ) ,
398+ )
399+ . filter_map ( |( package, sources) | {
400+ sources
401+ . iter ( )
402+ . any ( |source| matches ! ( source, Source :: Workspace { .. } ) )
403+ . then_some ( package. clone ( ) )
404+ } )
405+ . collect ( )
406+ }
407+
408+ /// Whether a given workspace member is required by another member.
409+ pub fn is_required_member ( & self , name : & PackageName ) -> bool {
410+ self . required_members ( ) . contains ( name)
411+ }
412+
335413 /// Returns the set of all workspace member dependency groups.
336414 pub fn group_requirements ( & self ) -> impl Iterator < Item = Requirement > + ' _ {
337- self . packages . values ( ) . filter_map ( |member| {
415+ self . packages . iter ( ) . filter_map ( |( name , member) | {
338416 let url = VerbatimUrl :: from_absolute_path ( & member. root )
339417 . expect ( "path is valid URL" )
340418 . with_given ( member. root . to_string_lossy ( ) ) ;
@@ -368,7 +446,10 @@ impl Workspace {
368446 extras : Box :: new ( [ ] ) ,
369447 groups : groups. into_boxed_slice ( ) ,
370448 marker : MarkerTree :: TRUE ,
371- source : if member. pyproject_toml . is_package ( ) {
449+ source : if member
450+ . pyproject_toml ( )
451+ . is_package ( !self . is_required_member ( name) )
452+ {
372453 RequirementSource :: Directory {
373454 install_path : member. root . clone ( ) . into_boxed_path ( ) ,
374455 editable : Some ( true ) ,
@@ -746,9 +827,16 @@ impl Workspace {
746827 . and_then ( |uv| uv. index )
747828 . unwrap_or_default ( ) ;
748829
830+ let required_members = Self :: collect_required_members (
831+ & workspace_members,
832+ & workspace_sources,
833+ & workspace_pyproject_toml,
834+ ) ;
835+
749836 Ok ( Workspace {
750837 install_path : workspace_root,
751838 packages : workspace_members,
839+ required_members,
752840 sources : workspace_sources,
753841 indexes : workspace_indexes,
754842 pyproject_toml : workspace_pyproject_toml,
@@ -1232,15 +1320,23 @@ impl ProjectWorkspace {
12321320 project. name . clone ( ) ,
12331321 current_project,
12341322 ) ] ) ) ;
1323+ let workspace_sources = BTreeMap :: default ( ) ;
1324+ let required_members = Workspace :: collect_required_members (
1325+ & current_project_as_members,
1326+ & workspace_sources,
1327+ project_pyproject_toml,
1328+ ) ;
1329+
12351330 return Ok ( Self {
12361331 project_root : project_path. clone ( ) ,
12371332 project_name : project. name . clone ( ) ,
12381333 workspace : Workspace {
12391334 install_path : project_path. clone ( ) ,
12401335 packages : current_project_as_members,
1336+ required_members,
12411337 // There may be package sources, but we don't need to duplicate them into the
12421338 // workspace sources.
1243- sources : BTreeMap :: default ( ) ,
1339+ sources : workspace_sources ,
12441340 indexes : Vec :: default ( ) ,
12451341 pyproject_toml : project_pyproject_toml. clone ( ) ,
12461342 } ,
@@ -1692,6 +1788,7 @@ mod tests {
16921788 "pyproject_toml": "[PYPROJECT_TOML]"
16931789 }
16941790 },
1791+ "required_members": [],
16951792 "sources": {},
16961793 "indexes": [],
16971794 "pyproject_toml": {
@@ -1745,6 +1842,7 @@ mod tests {
17451842 "pyproject_toml": "[PYPROJECT_TOML]"
17461843 }
17471844 },
1845+ "required_members": [],
17481846 "sources": {},
17491847 "indexes": [],
17501848 "pyproject_toml": {
@@ -1825,6 +1923,10 @@ mod tests {
18251923 "pyproject_toml": "[PYPROJECT_TOML]"
18261924 }
18271925 },
1926+ "required_members": [
1927+ "bird-feeder",
1928+ "seeds"
1929+ ],
18281930 "sources": {
18291931 "bird-feeder": [
18301932 {
@@ -1946,6 +2048,10 @@ mod tests {
19462048 "pyproject_toml": "[PYPROJECT_TOML]"
19472049 }
19482050 },
2051+ "required_members": [
2052+ "bird-feeder",
2053+ "seeds"
2054+ ],
19492055 "sources": {},
19502056 "indexes": [],
19512057 "pyproject_toml": {
@@ -2013,6 +2119,7 @@ mod tests {
20132119 "pyproject_toml": "[PYPROJECT_TOML]"
20142120 }
20152121 },
2122+ "required_members": [],
20162123 "sources": {},
20172124 "indexes": [],
20182125 "pyproject_toml": {
@@ -2147,6 +2254,7 @@ mod tests {
21472254 "pyproject_toml": "[PYPROJECT_TOML]"
21482255 }
21492256 },
2257+ "required_members": [],
21502258 "sources": {},
21512259 "indexes": [],
21522260 "pyproject_toml": {
@@ -2254,6 +2362,7 @@ mod tests {
22542362 "pyproject_toml": "[PYPROJECT_TOML]"
22552363 }
22562364 },
2365+ "required_members": [],
22572366 "sources": {},
22582367 "indexes": [],
22592368 "pyproject_toml": {
@@ -2375,6 +2484,7 @@ mod tests {
23752484 "pyproject_toml": "[PYPROJECT_TOML]"
23762485 }
23772486 },
2487+ "required_members": [],
23782488 "sources": {},
23792489 "indexes": [],
23802490 "pyproject_toml": {
@@ -2470,6 +2580,7 @@ mod tests {
24702580 "pyproject_toml": "[PYPROJECT_TOML]"
24712581 }
24722582 },
2583+ "required_members": [],
24732584 "sources": {},
24742585 "indexes": [],
24752586 "pyproject_toml": {
0 commit comments