mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 05:15: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))]
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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, ¤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]
|
||||
|
|
|
@ -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": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
@ -254,6 +261,7 @@
|
|||
}
|
||||
},
|
||||
"workspace": {
|
||||
"description": "The workspace definition for the project, if any.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolUvWorkspace"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue