Automatically detect workspace packages in uv add (#4557)

## Summary

If the package _isn't_ marked as `workspace = true`, locking will fail
given:

```rust
let workspace_package_declared =
    // We require that when you use a package that's part of the workspace, ...
    !workspace.packages().contains_key(&requirement.name)
    // ... it must be declared as a workspace dependency (`workspace = true`), ...
    || matches!(
        source,
        Some(Source::Workspace {
            // By using toml, we technically support `workspace = false`.
            workspace: true,
            ..
        })
    )
    // ... except for recursive self-inclusion (extras that activate other extras), e.g.
    // `framework[machine_learning]` depends on `framework[cuda]`.
    || &requirement.name == project_name;
if !workspace_package_declared {
    return Err(LoweringError::UndeclaredWorkspacePackage);
}
```

Closes https://github.com/astral-sh/uv/issues/4552.
This commit is contained in:
Charlie Marsh 2024-06-26 14:03:23 -04:00 committed by GitHub
parent a328c7b995
commit 45c271d15d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 46 additions and 23 deletions

View file

@ -1749,10 +1749,6 @@ pub struct AddArgs {
#[arg(long)]
pub dev: bool,
/// Add the requirements as workspace dependencies.
#[arg(long)]
pub workspace: bool,
/// Add the requirements as editables.
#[arg(long, default_missing_value = "true", num_args(0..=1))]
pub editable: Option<bool>,

View file

@ -194,14 +194,19 @@ pub enum Source {
#[derive(Error, Debug)]
pub enum SourceError {
#[error("Cannot resolve git reference `{0}`.")]
#[error("Cannot resolve git reference `{0}`")]
UnresolvedReference(String),
#[error("Workspace dependency must be a local path.")]
InvalidWorkspaceRequirement,
#[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
WorkspacePackageGit(String),
#[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
WorkspacePackageUrl(String),
#[error("Workspace dependency `{0}` must refer to local directory, not a file")]
WorkspacePackageFile(String),
}
impl Source {
pub fn from_requirement(
name: &PackageName,
source: RequirementSource,
workspace: bool,
editable: Option<bool>,
@ -210,15 +215,23 @@ impl Source {
branch: Option<String>,
) -> Result<Option<Source>, SourceError> {
if workspace {
match source {
RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {}
_ => return Err(SourceError::InvalidWorkspaceRequirement),
}
return Ok(Some(Source::Workspace {
editable,
workspace: true,
}));
return match source {
RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
Ok(Some(Source::Workspace {
editable,
workspace: true,
}))
}
RequirementSource::Url { .. } => {
Err(SourceError::WorkspacePackageUrl(name.to_string()))
}
RequirementSource::Git { .. } => {
Err(SourceError::WorkspacePackageGit(name.to_string()))
}
RequirementSource::Path { .. } => {
Err(SourceError::WorkspacePackageFile(name.to_string()))
}
};
}
let source = match source {

View file

@ -26,7 +26,6 @@ use crate::settings::ResolverInstallerSettings;
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub(crate) async fn add(
requirements: Vec<RequirementsSource>,
workspace: bool,
dev: bool,
editable: Option<bool>,
raw: bool,
@ -154,7 +153,9 @@ pub(crate) async fn add(
(pep508_rs::Requirement::from(req), None)
} else {
// Otherwise, try to construct the source.
let workspace = project.workspace().packages().contains_key(&req.name);
let result = Source::from_requirement(
&req.name,
req.source.clone(),
workspace,
editable,

View file

@ -719,7 +719,6 @@ async fn run() -> Result<ExitStatus> {
commands::add(
args.requirements,
args.workspace,
args.dev,
args.editable,
args.raw,

View file

@ -432,7 +432,6 @@ impl LockSettings {
pub(crate) struct AddSettings {
pub(crate) requirements: Vec<RequirementsSource>,
pub(crate) dev: bool,
pub(crate) workspace: bool,
pub(crate) editable: Option<bool>,
pub(crate) raw: bool,
pub(crate) rev: Option<String>,
@ -451,7 +450,6 @@ impl AddSettings {
let AddArgs {
requirements,
dev,
workspace,
editable,
raw,
rev,
@ -471,7 +469,6 @@ impl AddSettings {
Self {
requirements,
workspace,
dev,
editable,
raw,

View file

@ -735,11 +735,29 @@ fn add_remove_workspace() -> Result<()> {
dependencies = []
"#})?;
// Adding a workspace package with a mismatched source should error.
let mut add_cmd =
context.add(&["child2 @ git+https://github.com/astral-test/uv-public-pypackage"]);
add_cmd
.arg("--preview")
.arg("--package")
.arg("child1")
.current_dir(&context.temp_dir);
uv_snapshot!(context.filters(), add_cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Workspace dependency `child2` must refer to local directory, not a Git repository
"###);
// Workspace packages should be detected automatically.
let child1 = context.temp_dir.join("child1");
let mut add_cmd = context.add(&["child2"]);
add_cmd
.arg("--preview")
.arg("--workspace")
.arg("--package")
.arg("child1")
.current_dir(&context.temp_dir);
@ -921,7 +939,6 @@ fn add_workspace_editable() -> Result<()> {
let mut add_cmd = context.add(&["child2"]);
add_cmd
.arg("--editable")
.arg("--workspace")
.arg("--preview")
.current_dir(&child1);