@@ -94,12 +94,51 @@ pub enum MemberDiscovery {
94
94
Ignore ( BTreeSet < PathBuf > ) ,
95
95
}
96
96
97
+ /// Whether a "project" must be defined via a `[project]` table.
98
+ #[ derive( Debug , Default , Clone , Hash , PartialEq , Eq ) ]
99
+ pub enum ProjectDiscovery {
100
+ /// The `[project]` table is optional; when missing, the target is treated as virtual.
101
+ #[ default]
102
+ Optional ,
103
+ /// A `[project]` table must be defined, unless `[tool.uv.workspace]` is present indicating a
104
+ /// legacy non-project workspace root.
105
+ ///
106
+ /// If neither is defined, discovery will fail.
107
+ Legacy ,
108
+ /// A `[project]` table must be defined.
109
+ ///
110
+ /// If not defined, discovery will fail.
111
+ Required ,
112
+ }
113
+
114
+ impl ProjectDiscovery {
115
+ /// Whether a `[project]` table is required.
116
+ pub fn allows_implicit_workspace ( & self ) -> bool {
117
+ match self {
118
+ Self :: Optional => true ,
119
+ Self :: Legacy => false ,
120
+ Self :: Required => false ,
121
+ }
122
+ }
123
+
124
+ /// Whether a legacy workspace root is allowed.
125
+ pub fn allows_legacy_workspace ( & self ) -> bool {
126
+ match self {
127
+ Self :: Optional => true ,
128
+ Self :: Legacy => true ,
129
+ Self :: Required => false ,
130
+ }
131
+ }
132
+ }
133
+
97
134
#[ derive( Debug , Default , Clone , Hash , PartialEq , Eq ) ]
98
135
pub struct DiscoveryOptions {
99
136
/// The path to stop discovery at.
100
137
pub stop_discovery_at : Option < PathBuf > ,
101
138
/// The strategy to use when discovering workspace members.
102
139
pub members : MemberDiscovery ,
140
+ /// The strategy to use when discovering the project.
141
+ pub project : ProjectDiscovery ,
103
142
}
104
143
105
144
pub type RequiresPythonSources = BTreeMap < ( PackageName , Option < GroupName > ) , VersionSpecifiers > ;
@@ -1561,13 +1600,13 @@ fn is_included_in_workspace(
1561
1600
1562
1601
/// A project that can be discovered.
1563
1602
///
1564
- /// The project could be a package within a workspace, a real workspace root, or a (legacy)
1565
- /// non-project workspace root, which can define its own dev dependencies.
1603
+ /// The project could be a package within a workspace, a real workspace root, or a non-project
1604
+ /// workspace root, which can define its own dev dependencies.
1566
1605
#[ derive( Debug , Clone ) ]
1567
1606
pub enum VirtualProject {
1568
1607
/// A project (which could be a workspace root or member).
1569
1608
Project ( ProjectWorkspace ) ,
1570
- /// A (legacy) non-project workspace root.
1609
+ /// A non-project workspace root.
1571
1610
NonProject ( Workspace ) ,
1572
1611
}
1573
1612
@@ -1583,33 +1622,6 @@ impl VirtualProject {
1583
1622
path : & Path ,
1584
1623
options : & DiscoveryOptions ,
1585
1624
cache : & WorkspaceCache ,
1586
- ) -> Result < Self , WorkspaceError > {
1587
- Self :: discover_impl ( path, options, cache, false ) . await
1588
- }
1589
-
1590
- /// Equivalent to [`VirtualProject::discover`] but consider it acceptable for
1591
- /// both `[project]` and `[tool.uv.workspace]` to be missing.
1592
- ///
1593
- /// If they are, we act as if an empty `[tool.uv.workspace]` was found.
1594
- pub async fn discover_defaulted (
1595
- path : & Path ,
1596
- options : & DiscoveryOptions ,
1597
- cache : & WorkspaceCache ,
1598
- ) -> Result < Self , WorkspaceError > {
1599
- Self :: discover_impl ( path, options, cache, true ) . await
1600
- }
1601
-
1602
- /// Find the current project or virtual workspace root, given the current directory.
1603
- ///
1604
- /// Similar to calling [`ProjectWorkspace::discover`] with a fallback to [`Workspace::discover`],
1605
- /// but avoids rereading the `pyproject.toml` (and relying on error-handling as control flow).
1606
- ///
1607
- /// This method requires an absolute path and panics otherwise.
1608
- async fn discover_impl (
1609
- path : & Path ,
1610
- options : & DiscoveryOptions ,
1611
- cache : & WorkspaceCache ,
1612
- default_missing_workspace : bool ,
1613
1625
) -> Result < Self , WorkspaceError > {
1614
1626
assert ! (
1615
1627
path. is_absolute( ) ,
@@ -1656,6 +1668,7 @@ impl VirtualProject {
1656
1668
. as_ref ( )
1657
1669
. and_then ( |tool| tool. uv . as_ref ( ) )
1658
1670
. and_then ( |uv| uv. workspace . as_ref ( ) )
1671
+ . filter ( |_| options. project . allows_legacy_workspace ( ) )
1659
1672
{
1660
1673
// Otherwise, if it contains a `tool.uv.workspace` table, it's a non-project workspace
1661
1674
// root.
@@ -1674,7 +1687,7 @@ impl VirtualProject {
1674
1687
. await ?;
1675
1688
1676
1689
Ok ( Self :: NonProject ( workspace) )
1677
- } else if default_missing_workspace {
1690
+ } else if options . project . allows_implicit_workspace ( ) {
1678
1691
// Otherwise it's a pyproject.toml that maybe contains dependency-groups
1679
1692
// that we want to treat like a project/workspace to handle those uniformly
1680
1693
let project_path = std:: path:: absolute ( project_root)
0 commit comments