@@ -94,12 +94,51 @@ pub enum MemberDiscovery {
9494 Ignore ( BTreeSet < PathBuf > ) ,
9595}
9696
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+
97134#[ derive( Debug , Default , Clone , Hash , PartialEq , Eq ) ]
98135pub struct DiscoveryOptions {
99136 /// The path to stop discovery at.
100137 pub stop_discovery_at : Option < PathBuf > ,
101138 /// The strategy to use when discovering workspace members.
102139 pub members : MemberDiscovery ,
140+ /// The strategy to use when discovering the project.
141+ pub project : ProjectDiscovery ,
103142}
104143
105144pub type RequiresPythonSources = BTreeMap < ( PackageName , Option < GroupName > ) , VersionSpecifiers > ;
@@ -1561,13 +1600,13 @@ fn is_included_in_workspace(
15611600
15621601/// A project that can be discovered.
15631602///
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.
15661605#[ derive( Debug , Clone ) ]
15671606pub enum VirtualProject {
15681607 /// A project (which could be a workspace root or member).
15691608 Project ( ProjectWorkspace ) ,
1570- /// A (legacy) non-project workspace root.
1609+ /// A non-project workspace root.
15711610 NonProject ( Workspace ) ,
15721611}
15731612
@@ -1583,33 +1622,6 @@ impl VirtualProject {
15831622 path : & Path ,
15841623 options : & DiscoveryOptions ,
15851624 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 ,
16131625 ) -> Result < Self , WorkspaceError > {
16141626 assert ! (
16151627 path. is_absolute( ) ,
@@ -1656,6 +1668,7 @@ impl VirtualProject {
16561668 . as_ref ( )
16571669 . and_then ( |tool| tool. uv . as_ref ( ) )
16581670 . and_then ( |uv| uv. workspace . as_ref ( ) )
1671+ . filter ( |_| options. project . allows_legacy_workspace ( ) )
16591672 {
16601673 // Otherwise, if it contains a `tool.uv.workspace` table, it's a non-project workspace
16611674 // root.
@@ -1674,7 +1687,7 @@ impl VirtualProject {
16741687 . await ?;
16751688
16761689 Ok ( Self :: NonProject ( workspace) )
1677- } else if default_missing_workspace {
1690+ } else if options . project . allows_implicit_workspace ( ) {
16781691 // Otherwise it's a pyproject.toml that maybe contains dependency-groups
16791692 // that we want to treat like a project/workspace to handle those uniformly
16801693 let project_path = std:: path:: absolute ( project_root)
0 commit comments