mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
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:
parent
be2a67cd9b
commit
a4417eba4a
8 changed files with 120 additions and 0 deletions
|
@ -74,7 +74,11 @@ pub struct Tool {
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct ToolUv {
|
pub struct ToolUv {
|
||||||
pub sources: Option<BTreeMap<PackageName, Source>>,
|
pub sources: Option<BTreeMap<PackageName, Source>>,
|
||||||
|
/// The workspace definition for the project, if any.
|
||||||
pub workspace: Option<ToolUvWorkspace>,
|
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(
|
#[cfg_attr(
|
||||||
feature = "schemars",
|
feature = "schemars",
|
||||||
schemars(
|
schemars(
|
||||||
|
|
|
@ -25,6 +25,8 @@ pub enum WorkspaceError {
|
||||||
MissingProject(PathBuf),
|
MissingProject(PathBuf),
|
||||||
#[error("No workspace found for: `{}`", _0.simplified_display())]
|
#[error("No workspace found for: `{}`", _0.simplified_display())]
|
||||||
MissingWorkspace(PathBuf),
|
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}`")]
|
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
|
||||||
DynamicNotAllowed(&'static str),
|
DynamicNotAllowed(&'static str),
|
||||||
#[error("Failed to find directories for glob: `{0}`")]
|
#[error("Failed to find directories for glob: `{0}`")]
|
||||||
|
@ -83,6 +85,21 @@ impl Workspace {
|
||||||
.map_err(WorkspaceError::Normalize)?
|
.map_err(WorkspaceError::Normalize)?
|
||||||
.to_path_buf();
|
.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.
|
// Check if the current project is also an explicit workspace root.
|
||||||
let explicit_root = pyproject_toml
|
let explicit_root = pyproject_toml
|
||||||
.tool
|
.tool
|
||||||
|
@ -326,6 +343,21 @@ impl Workspace {
|
||||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
.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.
|
// Extract the package name.
|
||||||
let Some(project) = pyproject_toml.project.clone() else {
|
let Some(project) = pyproject_toml.project.clone() else {
|
||||||
return Err(WorkspaceError::MissingProject(member_root));
|
return Err(WorkspaceError::MissingProject(member_root));
|
||||||
|
@ -586,6 +618,18 @@ impl ProjectWorkspace {
|
||||||
.map_err(WorkspaceError::Normalize)?
|
.map_err(WorkspaceError::Normalize)?
|
||||||
.to_path_buf();
|
.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.
|
// Check if the current project is also an explicit workspace root.
|
||||||
let mut workspace = project_pyproject_toml
|
let mut workspace = project_pyproject_toml
|
||||||
.tool
|
.tool
|
||||||
|
|
|
@ -143,6 +143,7 @@ pub(crate) async fn run(
|
||||||
match VirtualProject::discover(&std::env::current_dir()?, None).await {
|
match VirtualProject::discover(&std::env::current_dir()?, None).await {
|
||||||
Ok(project) => Some(project),
|
Ok(project) => Some(project),
|
||||||
Err(WorkspaceError::MissingPyprojectToml) => None,
|
Err(WorkspaceError::MissingPyprojectToml) => None,
|
||||||
|
Err(WorkspaceError::NonWorkspace(_)) => None,
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -284,3 +284,34 @@ fn run_script() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
@ -167,6 +167,21 @@ fn test_albatross_project_in_excluded() {
|
||||||
);
|
);
|
||||||
|
|
||||||
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
|
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, ¤t_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]
|
#[test]
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[project]
|
||||||
|
name = "seeds"
|
||||||
|
version = "1.0.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["idna==3.6"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
managed = false
|
|
@ -0,0 +1,5 @@
|
||||||
|
import idna
|
||||||
|
|
||||||
|
|
||||||
|
def seeds():
|
||||||
|
print("sunflower")
|
8
uv.schema.json
generated
8
uv.schema.json
generated
|
@ -104,6 +104,13 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"managed": {
|
||||||
|
"description": "Whether the project is managed by `uv`. If `false`, `uv` will ignore the project when `uv run` is invoked.",
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"native-tls": {
|
"native-tls": {
|
||||||
"type": [
|
"type": [
|
||||||
"boolean",
|
"boolean",
|
||||||
|
@ -254,6 +261,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
|
"description": "The workspace definition for the project, if any.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/ToolUvWorkspace"
|
"$ref": "#/definitions/ToolUvWorkspace"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue