Enable projects to opt-out of workspace management (#4565)

## Summary

You can now add `managed = false` under `[tool.uv]` in a
`pyproject.toml` to explicitly opt out of the project and workspace
APIs.

If a project sets `managed = false`, we will (1) _not_ discover it as a
workspace root, and (2) _not_ discover it as a workspace member (similar
to using `exclude` in the workspace parent).

Closes https://github.com/astral-sh/uv/issues/4551.
This commit is contained in:
Charlie Marsh 2024-07-01 16:17:43 -04:00 committed by GitHub
parent be2a67cd9b
commit a4417eba4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 120 additions and 0 deletions

View file

@ -74,7 +74,11 @@ pub struct Tool {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ToolUv {
pub sources: Option<BTreeMap<PackageName, Source>>,
/// The workspace definition for the project, if any.
pub workspace: Option<ToolUvWorkspace>,
/// Whether the project is managed by `uv`. If `false`, `uv` will ignore the project when
/// `uv run` is invoked.
pub managed: Option<bool>,
#[cfg_attr(
feature = "schemars",
schemars(

View file

@ -25,6 +25,8 @@ pub enum WorkspaceError {
MissingProject(PathBuf),
#[error("No workspace found for: `{}`", _0.simplified_display())]
MissingWorkspace(PathBuf),
#[error("The project is marked as unmanaged: `{}`", _0.simplified_display())]
NonWorkspace(PathBuf),
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
DynamicNotAllowed(&'static str),
#[error("Failed to find directories for glob: `{0}`")]
@ -83,6 +85,21 @@ impl Workspace {
.map_err(WorkspaceError::Normalize)?
.to_path_buf();
// Check if the project is explicitly marked as unmanaged.
if pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.managed)
== Some(false)
{
debug!(
"Project `{}` is marked as unmanaged",
project_path.simplified_display()
);
return Err(WorkspaceError::NonWorkspace(project_path));
}
// Check if the current project is also an explicit workspace root.
let explicit_root = pyproject_toml
.tool
@ -326,6 +343,21 @@ impl Workspace {
let pyproject_toml = PyProjectToml::from_string(contents)
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
// Check if the current project is explicitly marked as unmanaged.
if pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.managed)
== Some(false)
{
debug!(
"Project `{}` is marked as unmanaged; omitting from workspace members",
pyproject_toml.project.as_ref().unwrap().name
);
continue;
}
// Extract the package name.
let Some(project) = pyproject_toml.project.clone() else {
return Err(WorkspaceError::MissingProject(member_root));
@ -586,6 +618,18 @@ impl ProjectWorkspace {
.map_err(WorkspaceError::Normalize)?
.to_path_buf();
// Check if workspaces are explicitly disabled for the project.
if project_pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.managed)
== Some(false)
{
debug!("Project `{}` is marked as unmanaged", project.name);
return Err(WorkspaceError::NonWorkspace(project_path));
}
// Check if the current project is also an explicit workspace root.
let mut workspace = project_pyproject_toml
.tool

View file

@ -143,6 +143,7 @@ pub(crate) async fn run(
match VirtualProject::discover(&std::env::current_dir()?, None).await {
Ok(project) => Some(project),
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(WorkspaceError::NonWorkspace(_)) => None,
Err(err) => return Err(err.into()),
}
};

View file

@ -284,3 +284,34 @@ fn run_script() -> Result<()> {
Ok(())
}
/// With `managed = false`, we should avoid installing the project itself.
#[test]
fn run_managed_false() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio"]
[tool.uv]
managed = false
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
warning: `uv run` is experimental and may change without warning.
"###);
Ok(())
}

View file

@ -167,6 +167,21 @@ fn test_albatross_project_in_excluded() {
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
let current_dir = workspaces_dir()
.join("albatross-project-in-excluded")
.join("packages")
.join("seeds");
uv_snapshot!(context.filters(), install_workspace(&context, &current_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to download and build: `seeds @ file://[WORKSPACE]/scripts/workspaces/albatross-project-in-excluded/packages/seeds`
Caused by: The project is marked as unmanaged: `[WORKSPACE]/scripts/workspaces/albatross-project-in-excluded/packages/seeds`
"###
);
}
#[test]