mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-11-04 05:34:28 +00:00 
			
		
		
		
	feat: error on non-existent extra from lock file (#11426)
Closes #10597. Recreated https://github.com/astral-sh/uv/pull/10925 that got closed as the base branch got merged. Snapshot tests. --------- Co-authored-by: Aria Desires <aria.desires@gmail.com>
This commit is contained in:
		
							parent
							
								
									49e10435f1
								
							
						
					
					
						commit
						b17a2ee61d
					
				
					 6 changed files with 375 additions and 7 deletions
				
			
		| 
						 | 
					@ -2617,6 +2617,11 @@ impl Package {
 | 
				
			||||||
    fn is_dynamic(&self) -> bool {
 | 
					    fn is_dynamic(&self) -> bool {
 | 
				
			||||||
        self.id.version.is_none()
 | 
					        self.id.version.is_none()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the extras the package provides, if any.
 | 
				
			||||||
 | 
					    pub fn provides_extras(&self) -> Option<&Vec<ExtraName>> {
 | 
				
			||||||
 | 
					        self.metadata.provides_extras.as_ref()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Attempts to construct a `VerbatimUrl` from the given normalized `Path`.
 | 
					/// Attempts to construct a `VerbatimUrl` from the given normalized `Path`.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,9 @@ use std::borrow::Cow;
 | 
				
			||||||
use std::path::Path;
 | 
					use std::path::Path;
 | 
				
			||||||
use std::str::FromStr;
 | 
					use std::str::FromStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use itertools::Either;
 | 
					use itertools::{Either, Itertools};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use uv_configuration::ExtrasSpecification;
 | 
				
			||||||
use uv_distribution_types::Index;
 | 
					use uv_distribution_types::Index;
 | 
				
			||||||
use uv_normalize::PackageName;
 | 
					use uv_normalize::PackageName;
 | 
				
			||||||
use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl};
 | 
					use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl};
 | 
				
			||||||
| 
						 | 
					@ -11,6 +13,8 @@ use uv_scripts::Pep723Script;
 | 
				
			||||||
use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources};
 | 
					use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources};
 | 
				
			||||||
use uv_workspace::Workspace;
 | 
					use uv_workspace::Workspace;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::commands::project::ProjectError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// A target that can be installed from a lockfile.
 | 
					/// A target that can be installed from a lockfile.
 | 
				
			||||||
#[derive(Debug, Copy, Clone)]
 | 
					#[derive(Debug, Copy, Clone)]
 | 
				
			||||||
pub(crate) enum InstallTarget<'lock> {
 | 
					pub(crate) enum InstallTarget<'lock> {
 | 
				
			||||||
| 
						 | 
					@ -230,4 +234,68 @@ impl<'lock> InstallTarget<'lock> {
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Validate the extras requested by the [`ExtrasSpecification`].
 | 
				
			||||||
 | 
					    #[allow(clippy::result_large_err)]
 | 
				
			||||||
 | 
					    pub(crate) fn validate_extras(self, extras: &ExtrasSpecification) -> Result<(), ProjectError> {
 | 
				
			||||||
 | 
					        let extras = match extras {
 | 
				
			||||||
 | 
					            ExtrasSpecification::Some(extras) => {
 | 
				
			||||||
 | 
					                if extras.is_empty() {
 | 
				
			||||||
 | 
					                    return Ok(());
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Either::Left(extras.iter())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ExtrasSpecification::Exclude(extras) => {
 | 
				
			||||||
 | 
					                if extras.is_empty() {
 | 
				
			||||||
 | 
					                    return Ok(());
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Either::Right(extras.iter())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => return Ok(()),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::Project { lock, .. }
 | 
				
			||||||
 | 
					            | Self::Workspace { lock, .. }
 | 
				
			||||||
 | 
					            | Self::NonProjectWorkspace { lock, .. } => {
 | 
				
			||||||
 | 
					                let member_packages: Vec<&Package> = lock
 | 
				
			||||||
 | 
					                    .packages()
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .filter(|package| self.roots().contains(package.name()))
 | 
				
			||||||
 | 
					                    .collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // If `provides-extra` is not set in any package, do not perform the check, as this
 | 
				
			||||||
 | 
					                // means that the lock file was generated on a version of uv that predates when the
 | 
				
			||||||
 | 
					                // feature was added.
 | 
				
			||||||
 | 
					                if !member_packages
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .any(|package| package.provides_extras().is_some())
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return Ok(());
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for extra in extras {
 | 
				
			||||||
 | 
					                    if !member_packages.iter().any(|package| {
 | 
				
			||||||
 | 
					                        package
 | 
				
			||||||
 | 
					                            .provides_extras()
 | 
				
			||||||
 | 
					                            .is_some_and(|provides_extras| provides_extras.contains(extra))
 | 
				
			||||||
 | 
					                    }) {
 | 
				
			||||||
 | 
					                        return match self {
 | 
				
			||||||
 | 
					                            Self::Project { .. } => {
 | 
				
			||||||
 | 
					                                Err(ProjectError::MissingExtraProject(extra.clone()))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            _ => Err(ProjectError::MissingExtraWorkspace(extra.clone())),
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Self::Script { .. } => {
 | 
				
			||||||
 | 
					                // We shouldn't get here if the list is empty so we can assume it isn't
 | 
				
			||||||
 | 
					                let extra = extras.into_iter().next().expect("non-empty extras").clone();
 | 
				
			||||||
 | 
					                return Err(ProjectError::MissingExtraScript(extra));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ use uv_distribution_types::{
 | 
				
			||||||
use uv_fs::{LockedFile, Simplified, CWD};
 | 
					use uv_fs::{LockedFile, Simplified, CWD};
 | 
				
			||||||
use uv_git::ResolvedRepositoryReference;
 | 
					use uv_git::ResolvedRepositoryReference;
 | 
				
			||||||
use uv_installer::{SatisfiesResult, SitePackages};
 | 
					use uv_installer::{SatisfiesResult, SitePackages};
 | 
				
			||||||
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
 | 
					use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
 | 
				
			||||||
use uv_pep440::{Version, VersionSpecifiers};
 | 
					use uv_pep440::{Version, VersionSpecifiers};
 | 
				
			||||||
use uv_pep508::MarkerTreeContents;
 | 
					use uv_pep508::MarkerTreeContents;
 | 
				
			||||||
use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts, Requirement};
 | 
					use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts, Requirement};
 | 
				
			||||||
| 
						 | 
					@ -152,6 +152,15 @@ pub(crate) enum ProjectError {
 | 
				
			||||||
    #[error("Default group `{0}` (from `tool.uv.default-groups`) is not defined in the project's `dependency-groups` table")]
 | 
					    #[error("Default group `{0}` (from `tool.uv.default-groups`) is not defined in the project's `dependency-groups` table")]
 | 
				
			||||||
    MissingDefaultGroup(GroupName),
 | 
					    MissingDefaultGroup(GroupName),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[error("Extra `{0}` is not defined in the project's `optional-dependencies` table")]
 | 
				
			||||||
 | 
					    MissingExtraProject(ExtraName),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[error("Extra `{0}` is not defined in any project's `optional-dependencies` table")]
 | 
				
			||||||
 | 
					    MissingExtraWorkspace(ExtraName),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[error("PEP 723 scripts do not support optional dependencies, but extra `{0}` was specified")]
 | 
				
			||||||
 | 
					    MissingExtraScript(ExtraName),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[error("Supported environments must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold())]
 | 
					    #[error("Supported environments must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold())]
 | 
				
			||||||
    OverlappingMarkers(String, String, String),
 | 
					    OverlappingMarkers(String, String, String),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -587,6 +587,7 @@ pub(super) async fn do_sync(
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate that the set of requested extras and development groups are compatible.
 | 
					    // Validate that the set of requested extras and development groups are compatible.
 | 
				
			||||||
 | 
					    target.validate_extras(extras)?;
 | 
				
			||||||
    detect_conflicts(target.lock(), extras, dev)?;
 | 
					    detect_conflicts(target.lock(), extras, dev)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Determine the markers to use for resolution.
 | 
					    // Determine the markers to use for resolution.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4440,11 +4440,18 @@ conflicts = [
 | 
				
			||||||
        "#,
 | 
					        "#,
 | 
				
			||||||
    )?;
 | 
					    )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // I believe there are multiple valid solutions here, but the main
 | 
					    // Error out, as x2 extra is only on the child.
 | 
				
			||||||
    // thing is that `x2` should _not_ activate the `idna==3.4` dependency
 | 
					 | 
				
			||||||
    // in `proxy1`. The `--extra=x2` should be a no-op, since there is no
 | 
					 | 
				
			||||||
    // `x2` extra in the top level `pyproject.toml`.
 | 
					 | 
				
			||||||
    uv_snapshot!(context.filters(), context.sync().arg("--extra=x2"), @r###"
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--extra=x2"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 7 packages in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `x2` is not defined in the project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync(), @r###"
 | 
				
			||||||
    success: true
 | 
					    success: true
 | 
				
			||||||
    exit_code: 0
 | 
					    exit_code: 0
 | 
				
			||||||
    ----- stdout -----
 | 
					    ----- stdout -----
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2626,6 +2626,151 @@ fn sync_group_self() -> Result<()> {
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn sync_non_existent_extra() -> Result<()> {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.12");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pyproject_toml = context.temp_dir.child("pyproject.toml");
 | 
				
			||||||
 | 
					    pyproject_toml.write_str(
 | 
				
			||||||
 | 
					        r#"
 | 
				
			||||||
 | 
					        [project]
 | 
				
			||||||
 | 
					        name = "project"
 | 
				
			||||||
 | 
					        version = "0.1.0"
 | 
				
			||||||
 | 
					        requires-python = ">=3.12"
 | 
				
			||||||
 | 
					        [project.optional-dependencies]
 | 
				
			||||||
 | 
					        types = ["sniffio>1"]
 | 
				
			||||||
 | 
					        async = ["anyio>3"]
 | 
				
			||||||
 | 
					        "#,
 | 
				
			||||||
 | 
					    )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context.lock().assert().success();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Requesting a non-existent extra should fail.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("baz"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 4 packages in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `baz` is not defined in the project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Excluding a non-existing extra when requesting all extras should fail.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-extras").arg("--no-extra").arg("baz"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 4 packages in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `baz` is not defined in the project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn sync_non_existent_extra_no_optional_dependencies() -> Result<()> {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.12");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pyproject_toml = context.temp_dir.child("pyproject.toml");
 | 
				
			||||||
 | 
					    pyproject_toml.write_str(
 | 
				
			||||||
 | 
					        r#"
 | 
				
			||||||
 | 
					        [project]
 | 
				
			||||||
 | 
					        name = "project"
 | 
				
			||||||
 | 
					        version = "0.1.0"
 | 
				
			||||||
 | 
					        requires-python = ">=3.12"
 | 
				
			||||||
 | 
					        "#,
 | 
				
			||||||
 | 
					    )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context.lock().assert().success();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Requesting a non-existent extra should fail.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("baz"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 1 package in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `baz` is not defined in the project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Excluding a non-existing extra when requesting all extras should fail.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-extras").arg("--no-extra").arg("baz"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 1 package in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `baz` is not defined in the project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Ensures that we do not perform validation of extras against a lock file that was generated on a
 | 
				
			||||||
 | 
					/// version of uv that predates when `provides-extras` feature was added.
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn sync_ignore_extras_check_when_no_provides_extras() -> Result<()> {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.12");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pyproject_toml = context.temp_dir.child("pyproject.toml");
 | 
				
			||||||
 | 
					    pyproject_toml.write_str(
 | 
				
			||||||
 | 
					        r#"
 | 
				
			||||||
 | 
					        [project]
 | 
				
			||||||
 | 
					        name = "project"
 | 
				
			||||||
 | 
					        version = "0.1.0"
 | 
				
			||||||
 | 
					        requires-python = ">=3.12"
 | 
				
			||||||
 | 
					        [project.optional-dependencies]
 | 
				
			||||||
 | 
					        types = ["sniffio>1"]
 | 
				
			||||||
 | 
					        "#,
 | 
				
			||||||
 | 
					    )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Write a lockfile that does not have `provides-extra`, simulating a version that predates when
 | 
				
			||||||
 | 
					    // the feature was added.
 | 
				
			||||||
 | 
					    context.temp_dir.child("uv.lock").write_str(indoc! {r#"
 | 
				
			||||||
 | 
					        version = 1
 | 
				
			||||||
 | 
					        requires-python = ">=3.12"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [[package]]
 | 
				
			||||||
 | 
					        name = "project"
 | 
				
			||||||
 | 
					        version = "0.1.0"
 | 
				
			||||||
 | 
					        source = { virtual = "." }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [package.optional-dependencies]
 | 
				
			||||||
 | 
					        types = [
 | 
				
			||||||
 | 
					            { name = "sniffio" },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [package.metadata]
 | 
				
			||||||
 | 
					        requires-dist = [{ name = "sniffio", marker = "extra == 'types'", specifier = ">1" }]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [[package]]
 | 
				
			||||||
 | 
					        name = "sniffio"
 | 
				
			||||||
 | 
					        version = "1.3.1"
 | 
				
			||||||
 | 
					        source = { registry = "https://pypi.org/simple" }
 | 
				
			||||||
 | 
					        sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
 | 
				
			||||||
 | 
					        wheels = [
 | 
				
			||||||
 | 
					            { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    "#})?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Requesting a non-existent extra should not fail, as no validation should be performed.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra").arg("baz"), @r###"
 | 
				
			||||||
 | 
					    success: true
 | 
				
			||||||
 | 
					    exit_code: 0
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Audited in [TIME]
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Regression test for <https://github.com/astral-sh/uv/issues/6316>.
 | 
					/// Regression test for <https://github.com/astral-sh/uv/issues/6316>.
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// Previously, we would read metadata statically from pyproject.toml and write that to `uv.lock`. In
 | 
					/// Previously, we would read metadata statically from pyproject.toml and write that to `uv.lock`. In
 | 
				
			||||||
| 
						 | 
					@ -5397,7 +5542,7 @@ fn sync_all_extras() -> Result<()> {
 | 
				
			||||||
     + typing-extensions==4.10.0
 | 
					     + typing-extensions==4.10.0
 | 
				
			||||||
    "###);
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Sync all extras.
 | 
					    // Sync all extras excluding an extra that exists in both the parent and child.
 | 
				
			||||||
    uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("types"), @r###"
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("types"), @r###"
 | 
				
			||||||
    success: true
 | 
					    success: true
 | 
				
			||||||
    exit_code: 0
 | 
					    exit_code: 0
 | 
				
			||||||
| 
						 | 
					@ -5409,6 +5554,139 @@ fn sync_all_extras() -> Result<()> {
 | 
				
			||||||
     - typing-extensions==4.10.0
 | 
					     - typing-extensions==4.10.0
 | 
				
			||||||
    "###);
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync an extra that doesn't exist.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("foo"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 8 packages in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `foo` is not defined in any project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync all extras excluding an extra that doesn't exist.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("foo"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 8 packages in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `foo` is not defined in any project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Sync all members in a workspace with dynamic extras.
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn sync_all_extras_dynamic() -> Result<()> {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.12");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pyproject_toml = context.temp_dir.child("pyproject.toml");
 | 
				
			||||||
 | 
					    pyproject_toml.write_str(
 | 
				
			||||||
 | 
					        r#"
 | 
				
			||||||
 | 
					        [project]
 | 
				
			||||||
 | 
					        name = "project"
 | 
				
			||||||
 | 
					        version = "0.1.0"
 | 
				
			||||||
 | 
					        requires-python = ">=3.12"
 | 
				
			||||||
 | 
					        dependencies = ["child"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [project.optional-dependencies]
 | 
				
			||||||
 | 
					        types = ["sniffio>1"]
 | 
				
			||||||
 | 
					        async = ["anyio>3"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [build-system]
 | 
				
			||||||
 | 
					        requires = ["setuptools>=42"]
 | 
				
			||||||
 | 
					        build-backend = "setuptools.build_meta"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [tool.uv.workspace]
 | 
				
			||||||
 | 
					        members = ["child"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [tool.uv.sources]
 | 
				
			||||||
 | 
					        child = { workspace = true }
 | 
				
			||||||
 | 
					        "#,
 | 
				
			||||||
 | 
					    )?;
 | 
				
			||||||
 | 
					    context
 | 
				
			||||||
 | 
					        .temp_dir
 | 
				
			||||||
 | 
					        .child("src")
 | 
				
			||||||
 | 
					        .child("project")
 | 
				
			||||||
 | 
					        .child("__init__.py")
 | 
				
			||||||
 | 
					        .touch()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add a workspace member.
 | 
				
			||||||
 | 
					    let child = context.temp_dir.child("child");
 | 
				
			||||||
 | 
					    child.child("pyproject.toml").write_str(
 | 
				
			||||||
 | 
					        r#"
 | 
				
			||||||
 | 
					        [project]
 | 
				
			||||||
 | 
					        name = "child"
 | 
				
			||||||
 | 
					        version = "0.1.0"
 | 
				
			||||||
 | 
					        requires-python = ">=3.12"
 | 
				
			||||||
 | 
					        dynamic = ["optional-dependencies"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [tool.setuptools.dynamic.optional-dependencies]
 | 
				
			||||||
 | 
					        dev = { file = "requirements-dev.txt" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [build-system]
 | 
				
			||||||
 | 
					        requires = ["setuptools>=42"]
 | 
				
			||||||
 | 
					        build-backend = "setuptools.build_meta"
 | 
				
			||||||
 | 
					        "#,
 | 
				
			||||||
 | 
					    )?;
 | 
				
			||||||
 | 
					    child
 | 
				
			||||||
 | 
					        .child("src")
 | 
				
			||||||
 | 
					        .child("child")
 | 
				
			||||||
 | 
					        .child("__init__.py")
 | 
				
			||||||
 | 
					        .touch()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    child
 | 
				
			||||||
 | 
					        .child("requirements-dev.txt")
 | 
				
			||||||
 | 
					        .write_str("typing-extensions==4.10.0")?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Generate a lockfile.
 | 
				
			||||||
 | 
					    context.lock().assert().success();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync an extra that exists in the parent.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r###"
 | 
				
			||||||
 | 
					    success: true
 | 
				
			||||||
 | 
					    exit_code: 0
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 6 packages in [TIME]
 | 
				
			||||||
 | 
					    Prepared 3 packages in [TIME]
 | 
				
			||||||
 | 
					    Installed 3 packages in [TIME]
 | 
				
			||||||
 | 
					     + child==0.1.0 (from file://[TEMP_DIR]/child)
 | 
				
			||||||
 | 
					     + project==0.1.0 (from file://[TEMP_DIR]/)
 | 
				
			||||||
 | 
					     + sniffio==1.3.1
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync a dynamic extra that exists in the child.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("dev"), @r###"
 | 
				
			||||||
 | 
					    success: true
 | 
				
			||||||
 | 
					    exit_code: 0
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 6 packages in [TIME]
 | 
				
			||||||
 | 
					    Prepared 1 package in [TIME]
 | 
				
			||||||
 | 
					    Uninstalled 1 package in [TIME]
 | 
				
			||||||
 | 
					    Installed 1 package in [TIME]
 | 
				
			||||||
 | 
					     - sniffio==1.3.1
 | 
				
			||||||
 | 
					     + typing-extensions==4.10.0
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync a dynamic extra that doesn't exist in the child.
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("foo"), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved 6 packages in [TIME]
 | 
				
			||||||
 | 
					    error: Extra `foo` is not defined in any project's `optional-dependencies` table
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue