mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 04:48:18 +00:00
Rename virtual workspace roots to non-project workspace roots (#6717)
## Summary Closes https://github.com/astral-sh/uv/issues/6709.
This commit is contained in:
parent
c1e831881b
commit
3ee6ca31f4
8 changed files with 78 additions and 64 deletions
|
|
@ -454,7 +454,7 @@ impl Lock {
|
|||
}
|
||||
|
||||
// Add any dependency groups that are exclusive to the workspace root (e.g., dev
|
||||
// dependencies in virtual workspaces).
|
||||
// dependencies in (legacy) non-project workspace roots).
|
||||
for group in dev {
|
||||
for dependency in project.group(group) {
|
||||
if dependency.marker.evaluate(marker_env, &[]) {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ impl Workspace {
|
|||
/// Find the workspace containing the given path.
|
||||
///
|
||||
/// Unlike the [`ProjectWorkspace`] discovery, this does not require a current project. It also
|
||||
/// always uses absolute path, i.e. this method only supports discovering the main workspace.
|
||||
/// always uses absolute path, i.e., this method only supports discovering the main workspace.
|
||||
///
|
||||
/// Steps of workspace discovery: Start by looking at the closest `pyproject.toml`:
|
||||
/// * If it's an explicit workspace root: Collect workspace from this root, we're done.
|
||||
|
|
@ -83,6 +83,16 @@ impl Workspace {
|
|||
/// * Otherwise, try to find an explicit workspace root above:
|
||||
/// * If an explicit workspace root exists: Collect workspace from this root, we're done.
|
||||
/// * If there is no explicit workspace: We have a single project workspace, we're done.
|
||||
///
|
||||
/// Note that there are two kinds of workspace roots: projects, and (legacy) non-project roots.
|
||||
/// The non-project roots lack a `[project]` table, and so are not themselves projects, as in:
|
||||
/// ```toml
|
||||
/// [tool.uv.workspace]
|
||||
/// members = ["packages/*"]
|
||||
///
|
||||
/// [tool.uv]
|
||||
/// dev-dependencies = ["ruff"]
|
||||
/// ```
|
||||
pub async fn discover(
|
||||
path: &Path,
|
||||
options: &DiscoveryOptions<'_>,
|
||||
|
|
@ -157,7 +167,7 @@ impl Workspace {
|
|||
|
||||
check_nested_workspaces(&workspace_root, options);
|
||||
|
||||
// Unlike in `ProjectWorkspace` discovery, we might be in a virtual workspace root without
|
||||
// Unlike in `ProjectWorkspace` discovery, we might be in a legacy non-project root without
|
||||
// being in any specific project.
|
||||
let current_project = pyproject_toml
|
||||
.project
|
||||
|
|
@ -232,19 +242,14 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the workspace has a virtual root.
|
||||
pub fn is_virtual(&self) -> bool {
|
||||
/// Returns `true` if the workspace has a (legacy) non-project root.
|
||||
pub fn is_non_project(&self) -> bool {
|
||||
!self
|
||||
.packages
|
||||
.values()
|
||||
.any(|member| *member.root() == self.install_path)
|
||||
}
|
||||
|
||||
/// Returns `true` if the workspace consists solely of a virtual root.
|
||||
pub fn only_virtual(&self) -> bool {
|
||||
self.packages.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the set of requirements that include all packages in the workspace.
|
||||
pub fn members_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
|
||||
self.packages.values().filter_map(|member| {
|
||||
|
|
@ -289,21 +294,21 @@ impl Workspace {
|
|||
/// Returns any requirements that are exclusive to the workspace root, i.e., not included in
|
||||
/// any of the workspace members.
|
||||
///
|
||||
/// For virtual workspaces, returns the dev dependencies in the workspace root, which are
|
||||
/// the only dependencies that are not part of the workspace members.
|
||||
/// For workspaces with non-project roots, returns the dev dependencies in the corresponding
|
||||
/// `pyproject.toml`.
|
||||
///
|
||||
/// For non-virtual workspaces, returns an empty list.
|
||||
pub fn root_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
|
||||
/// Otherwise, returns an empty list.
|
||||
pub fn non_project_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
|
||||
if self
|
||||
.packages
|
||||
.values()
|
||||
.any(|member| *member.root() == self.install_path)
|
||||
{
|
||||
// If the workspace is non-virtual, the root is a member, so we don't need to include
|
||||
// any root-only requirements.
|
||||
// If the workspace has an explicit root, the root is a member, so we don't need to
|
||||
// include any root-only requirements.
|
||||
Either::Left(std::iter::empty())
|
||||
} else {
|
||||
// Otherwise, return the dev dependencies in the workspace root.
|
||||
// Otherwise, return the dev dependencies in the non-project workspace root.
|
||||
Either::Right(
|
||||
self.pyproject_toml
|
||||
.tool
|
||||
|
|
@ -1166,14 +1171,14 @@ fn is_included_in_workspace(
|
|||
|
||||
/// A project that can be synced.
|
||||
///
|
||||
/// The project could be a package within a workspace, a real workspace root, or even a virtual
|
||||
/// workspace root.
|
||||
/// The project could be a package within a workspace, a real workspace root, or a (legacy)
|
||||
/// non-project workspace root, which can define its own dev dependencies.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VirtualProject {
|
||||
/// A project (which could be within a workspace, or an implicit workspace root).
|
||||
/// A project (which could be a workspace root or member).
|
||||
Project(ProjectWorkspace),
|
||||
/// A virtual workspace root.
|
||||
Virtual(Workspace),
|
||||
/// A (legacy) non-project workspace root.
|
||||
NonProject(Workspace),
|
||||
}
|
||||
|
||||
impl VirtualProject {
|
||||
|
|
@ -1227,7 +1232,8 @@ impl VirtualProject {
|
|||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.workspace.as_ref())
|
||||
{
|
||||
// Otherwise, if it contains a `tool.uv.workspace` table, it's a virtual workspace.
|
||||
// Otherwise, if it contains a `tool.uv.workspace` table, it's a non-project workspace
|
||||
// root.
|
||||
let project_path = std::path::absolute(project_root)
|
||||
.map_err(WorkspaceError::Normalize)?
|
||||
.clone();
|
||||
|
|
@ -1243,7 +1249,7 @@ impl VirtualProject {
|
|||
)
|
||||
.await?;
|
||||
|
||||
Ok(Self::Virtual(workspace))
|
||||
Ok(Self::NonProject(workspace))
|
||||
} else {
|
||||
Err(WorkspaceError::MissingProject(pyproject_path))
|
||||
}
|
||||
|
|
@ -1258,10 +1264,10 @@ impl VirtualProject {
|
|||
VirtualProject::Project(project) => Some(VirtualProject::Project(
|
||||
project.with_pyproject_toml(pyproject_toml)?,
|
||||
)),
|
||||
VirtualProject::Virtual(workspace) => {
|
||||
// If the project is virtual, the root isn't a member, so we can just update the
|
||||
// top-level `pyproject.toml`.
|
||||
Some(VirtualProject::Virtual(Workspace {
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
// If this is a non-project workspace root, then by definition the root isn't a
|
||||
// member, so we can just update the top-level `pyproject.toml`.
|
||||
Some(VirtualProject::NonProject(Workspace {
|
||||
pyproject_toml,
|
||||
..workspace.clone()
|
||||
}))
|
||||
|
|
@ -1273,7 +1279,7 @@ impl VirtualProject {
|
|||
pub fn root(&self) -> &Path {
|
||||
match self {
|
||||
VirtualProject::Project(project) => project.project_root(),
|
||||
VirtualProject::Virtual(workspace) => workspace.install_path(),
|
||||
VirtualProject::NonProject(workspace) => workspace.install_path(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1281,7 +1287,7 @@ impl VirtualProject {
|
|||
pub fn pyproject_toml(&self) -> &PyProjectToml {
|
||||
match self {
|
||||
VirtualProject::Project(project) => project.current_project().pyproject_toml(),
|
||||
VirtualProject::Virtual(workspace) => &workspace.pyproject_toml,
|
||||
VirtualProject::NonProject(workspace) => &workspace.pyproject_toml,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1289,7 +1295,7 @@ impl VirtualProject {
|
|||
pub fn workspace(&self) -> &Workspace {
|
||||
match self {
|
||||
VirtualProject::Project(project) => project.workspace(),
|
||||
VirtualProject::Virtual(workspace) => workspace,
|
||||
VirtualProject::NonProject(workspace) => workspace,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1299,7 +1305,7 @@ impl VirtualProject {
|
|||
VirtualProject::Project(project) => {
|
||||
Either::Left(std::iter::once(project.project_name()))
|
||||
}
|
||||
VirtualProject::Virtual(workspace) => Either::Right(workspace.packages().keys()),
|
||||
VirtualProject::NonProject(workspace) => Either::Right(workspace.packages().keys()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1314,11 +1320,11 @@ impl VirtualProject {
|
|||
) -> impl Iterator<Item = &pep508_rs::Requirement<VerbatimParsedUrl>> {
|
||||
match self {
|
||||
VirtualProject::Project(_) => {
|
||||
// For non-virtual projects, dev dependencies are attached to the members.
|
||||
// For projects, dev dependencies are attached to the members.
|
||||
Either::Left(std::iter::empty())
|
||||
}
|
||||
VirtualProject::Virtual(workspace) => {
|
||||
// For virtual projects, we might have dev dependencies that are attached to the
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
// For non-projects, we might have dev dependencies that are attached to the
|
||||
// workspace root (which isn't a member).
|
||||
if name == &*DEV_DEPENDENCIES {
|
||||
Either::Right(
|
||||
|
|
@ -1339,17 +1345,17 @@ impl VirtualProject {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the [`PackageName`] of the project, if it's not a virtual workspace root.
|
||||
/// Return the [`PackageName`] of the project, if available.
|
||||
pub fn project_name(&self) -> Option<&PackageName> {
|
||||
match self {
|
||||
VirtualProject::Project(project) => Some(project.project_name()),
|
||||
VirtualProject::Virtual(_) => None,
|
||||
VirtualProject::NonProject(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the project is a virtual workspace root.
|
||||
pub fn is_virtual(&self) -> bool {
|
||||
matches!(self, VirtualProject::Virtual(_))
|
||||
pub fn is_non_project(&self) -> bool {
|
||||
matches!(self, VirtualProject::NonProject(_))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -200,14 +200,14 @@ pub(crate) async fn add(
|
|||
VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await?
|
||||
};
|
||||
|
||||
// For virtual projects, allow dev dependencies, but nothing else.
|
||||
if project.is_virtual() {
|
||||
// For non-project workspace roots, allow dev dependencies, but nothing else.
|
||||
if project.is_non_project() {
|
||||
match dependency_type {
|
||||
DependencyType::Production => {
|
||||
anyhow::bail!("Found a virtual workspace root, but virtual projects do not support production dependencies (instead, use: `{}`)", "uv add --dev".green())
|
||||
bail!("Found a non-project workspace root; production dependencies are unsupported (instead, use: `{}`)", "uv add --dev".green())
|
||||
}
|
||||
DependencyType::Optional(_) => {
|
||||
anyhow::bail!("Found a virtual workspace root, but virtual projects do not support optional dependencies (instead, use: `{}`)", "uv add --dev".green())
|
||||
bail!("Found a non-project workspace root; optional dependencies are unsupported (instead, use: `{}`)", "uv add --dev".green())
|
||||
}
|
||||
DependencyType::Dev => (),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use uv_resolver::{
|
|||
ResolverMarkers, SatisfiesResult,
|
||||
};
|
||||
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace};
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
|
||||
|
|
@ -248,7 +248,7 @@ async fn do_lock(
|
|||
} = settings;
|
||||
|
||||
// Collect the requirements, etc.
|
||||
let requirements = workspace.root_requirements().collect::<Vec<_>>();
|
||||
let requirements = workspace.non_project_requirements().collect::<Vec<_>>();
|
||||
let overrides = workspace.overrides().into_iter().collect::<Vec<_>>();
|
||||
let constraints = workspace.constraints();
|
||||
let dev = vec![DEV_DEPENDENCIES.clone()];
|
||||
|
|
@ -262,7 +262,7 @@ async fn do_lock(
|
|||
// If this is a non-virtual project with a single member, we can omit it from the lockfile.
|
||||
// If any members are added or removed, it will inherently mismatch. If the member is
|
||||
// renamed, it will also mismatch.
|
||||
if members.len() == 1 && !workspace.is_virtual() {
|
||||
if members.len() == 1 && !workspace.is_non_project() {
|
||||
members.clear();
|
||||
}
|
||||
|
||||
|
|
@ -313,19 +313,15 @@ async fn do_lock(
|
|||
if requires_python.is_unbounded() {
|
||||
let default =
|
||||
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version());
|
||||
warn_user!("The workspace `requires-python` value does not contain a lower bound: `{requires_python}`. Set a lower bound to indicate the minimum compatible Python version (e.g., `{default}`).");
|
||||
warn_user_once!("The workspace `requires-python` value does not contain a lower bound: `{requires_python}`. Set a lower bound to indicate the minimum compatible Python version (e.g., `{default}`).");
|
||||
}
|
||||
requires_python
|
||||
} else {
|
||||
let default =
|
||||
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version());
|
||||
if workspace.only_virtual() {
|
||||
debug!("No `requires-python` in virtual-only workspace. Defaulting to `{default}`.");
|
||||
} else {
|
||||
warn_user!(
|
||||
"No `requires-python` value found in the workspace. Defaulting to `{default}`."
|
||||
);
|
||||
}
|
||||
warn_user_once!(
|
||||
"No `requires-python` value found in the workspace. Defaulting to `{default}`."
|
||||
);
|
||||
default
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec
|
|||
let requires_python = find_requires_python(project_workspace.workspace())?;
|
||||
(requires_python, "project")
|
||||
}
|
||||
VirtualProject::Virtual(workspace) => {
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
debug!(
|
||||
"Discovered virtual workspace at: {}",
|
||||
workspace.install_path().display()
|
||||
|
|
|
|||
|
|
@ -3511,9 +3511,9 @@ fn add_lower_bound_local() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Add dependencies to a virtual workspace root.
|
||||
/// Add dependencies to a (legacy) non-project workspace root.
|
||||
#[test]
|
||||
fn add_virtual() -> Result<()> {
|
||||
fn add_non_project() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
|
|
@ -3530,7 +3530,7 @@ fn add_virtual() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Found a virtual workspace root, but virtual projects do not support production dependencies (instead, use: `uv add --dev`)
|
||||
error: Found a non-project workspace root; production dependencies are unsupported (instead, use: `uv add --dev`)
|
||||
"###);
|
||||
|
||||
// Adding `iniconfig` as optional should fail, since virtual workspace roots don't support
|
||||
|
|
@ -3541,7 +3541,7 @@ fn add_virtual() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Found a virtual workspace root, but virtual projects do not support optional dependencies (instead, use: `uv add --dev`)
|
||||
error: Found a non-project workspace root; optional dependencies are unsupported (instead, use: `uv add --dev`)
|
||||
"###);
|
||||
|
||||
// Adding `iniconfig` as a dev dependency should succeed.
|
||||
|
|
@ -3551,6 +3551,7 @@ fn add_virtual() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
|
|
|
|||
|
|
@ -9463,7 +9463,7 @@ fn lock_new_constraints() -> Result<()> {
|
|||
/// Lock a `pyproject.toml`, add a new constraint, and ensure that the lockfile is updated on the
|
||||
/// next run.
|
||||
#[test]
|
||||
fn lock_remove_member_virtual() -> Result<()> {
|
||||
fn lock_remove_member_non_project() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create a virtual workspace root.
|
||||
|
|
@ -9592,6 +9592,7 @@ fn lock_remove_member_virtual() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved in [TIME]
|
||||
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
"###);
|
||||
|
|
@ -9603,6 +9604,7 @@ fn lock_remove_member_virtual() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved in [TIME]
|
||||
Removed anyio v4.3.0
|
||||
Removed idna v3.6
|
||||
|
|
@ -10656,9 +10658,9 @@ fn lock_overlapping_environment() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock a virtual project with forked dev dependencies.
|
||||
/// Lock a (legacy) non-project workspace root with forked dev dependencies.
|
||||
#[test]
|
||||
fn lock_virtual_fork() -> Result<()> {
|
||||
fn lock_non_project_fork() -> Result<()> {
|
||||
let context = TestContext::new("3.10");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
|
|
@ -10681,6 +10683,7 @@ fn lock_virtual_fork() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.10`.
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
|
|
@ -10787,6 +10790,7 @@ fn lock_virtual_fork() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.10`.
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
|
|
@ -10798,6 +10802,7 @@ fn lock_virtual_fork() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.10`.
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
|
|
@ -10822,6 +10827,7 @@ fn lock_virtual_fork() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.10`.
|
||||
Resolved 7 packages in [TIME]
|
||||
Added iniconfig v2.0.0
|
||||
"###);
|
||||
|
|
@ -10845,9 +10851,9 @@ fn lock_virtual_fork() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock a virtual project with a conditional dependency.
|
||||
/// Lock a (legacy) non-project workspace root with a conditional dependency.
|
||||
#[test]
|
||||
fn lock_virtual_conditional() -> Result<()> {
|
||||
fn lock_non_project_conditional() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
|
|
@ -10867,6 +10873,7 @@ fn lock_virtual_conditional() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
|
|
@ -10927,6 +10934,7 @@ fn lock_virtual_conditional() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
|
|
@ -10938,6 +10946,7 @@ fn lock_virtual_conditional() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ fn empty() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved in [TIME]
|
||||
Audited in [TIME]
|
||||
"###);
|
||||
|
|
@ -211,6 +212,7 @@ fn empty() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||||
Resolved in [TIME]
|
||||
Audited in [TIME]
|
||||
"###);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue