Skip to content

Commit 1943aba

Browse files
Gankrazanieb
andauthored
Allow [project] to be missing from a pyproject.toml (#14113)
Closes #8666 Closes #6838 --------- Co-authored-by: Zanie Blue <[email protected]>
1 parent fa50a5c commit 1943aba

File tree

12 files changed

+916
-46
lines changed

12 files changed

+916
-46
lines changed

crates/uv-distribution/src/metadata/dependency_groups.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,15 @@ impl SourcedDependencyGroups {
7777
SourceStrategy::Enabled => MemberDiscovery::default(),
7878
SourceStrategy::Disabled => MemberDiscovery::None,
7979
},
80+
..DiscoveryOptions::default()
8081
};
8182

8283
// The subsequent API takes an absolute path to the dir the pyproject is in
8384
let empty = PathBuf::new();
8485
let absolute_pyproject_path =
8586
std::path::absolute(pyproject_path).map_err(WorkspaceError::Normalize)?;
8687
let project_dir = absolute_pyproject_path.parent().unwrap_or(&empty);
87-
let project = VirtualProject::discover_defaulted(project_dir, &discovery, cache).await?;
88+
let project = VirtualProject::discover(project_dir, &discovery, cache).await?;
8889

8990
// Collect the dependency groups.
9091
let dependency_groups =

crates/uv-distribution/src/metadata/requires_dist.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ impl RequiresDist {
6161
SourceStrategy::Enabled => MemberDiscovery::default(),
6262
SourceStrategy::Disabled => MemberDiscovery::None,
6363
},
64+
..DiscoveryOptions::default()
6465
};
6566
let Some(project_workspace) =
6667
ProjectWorkspace::from_maybe_project_root(install_path, &discovery, cache).await?

crates/uv-workspace/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub use workspace::{
2-
DiscoveryOptions, Editability, MemberDiscovery, ProjectWorkspace, RequiresPythonSources,
3-
VirtualProject, Workspace, WorkspaceCache, WorkspaceError, WorkspaceMember,
2+
DiscoveryOptions, Editability, MemberDiscovery, ProjectDiscovery, ProjectWorkspace,
3+
RequiresPythonSources, VirtualProject, Workspace, WorkspaceCache, WorkspaceError,
4+
WorkspaceMember,
45
};
56

67
pub mod dependency_groups;

crates/uv-workspace/src/workspace.rs

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
98135
pub 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

105144
pub 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)]
15671606
pub 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)

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,10 @@ async fn find_target(
358358
VirtualProject::Project(
359359
Workspace::discover(
360360
project_dir,
361-
&DiscoveryOptions::default(),
361+
&DiscoveryOptions {
362+
project: uv_workspace::ProjectDiscovery::Required,
363+
..DiscoveryOptions::default()
364+
},
362365
&WorkspaceCache::default(),
363366
)
364367
.await
@@ -369,7 +372,10 @@ async fn find_target(
369372
} else {
370373
VirtualProject::discover(
371374
project_dir,
372-
&DiscoveryOptions::default(),
375+
&DiscoveryOptions {
376+
project: uv_workspace::ProjectDiscovery::Required,
377+
..DiscoveryOptions::default()
378+
},
373379
&WorkspaceCache::default(),
374380
)
375381
.await

0 commit comments

Comments
 (0)