diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 52ba487c0..dc649f549 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -190,24 +190,24 @@ impl<'a> SitePackages<'a> { } // Verify that the dependencies are installed. - for requirement in &metadata.requires_dist { - if !requirement.evaluate_markers(self.venv.interpreter().markers(), &[]) { + for dependency in &metadata.requires_dist { + if !dependency.evaluate_markers(self.venv.interpreter().markers(), &[]) { continue; } let Some(installed) = self .by_name - .get(&requirement.name) + .get(&dependency.name) .map(|idx| &self.distributions[*idx]) else { diagnostics.push(Diagnostic::MissingDependency { package: package.clone(), - requirement: requirement.clone(), + requirement: dependency.clone(), }); continue; }; - match &requirement.version_or_url { + match &dependency.version_or_url { None | Some(pep508_rs::VersionOrUrl::Url(_)) => { // Nothing to do (accept any installed version). } @@ -216,7 +216,7 @@ impl<'a> SitePackages<'a> { diagnostics.push(Diagnostic::IncompatibleDependency { package: package.clone(), version: installed.version().clone(), - requirement: requirement.clone(), + requirement: dependency.clone(), }); } } @@ -234,10 +234,19 @@ impl<'a> SitePackages<'a> { editables: &[EditableRequirement], constraints: &[Requirement], ) -> Result { - let mut requirements = requirements.to_vec(); + let mut stack = Vec::::with_capacity(requirements.len()); let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), BuildHasherDefault::default()); + // Add the direct requirements to the queue. + for dependency in requirements { + if dependency.evaluate_markers(self.venv.interpreter().markers(), &[]) { + if seen.insert(dependency.clone()) { + stack.push(dependency.clone()); + } + } + } + // Verify that all editable requirements are met. for requirement in editables { let Some(distribution) = self @@ -253,15 +262,21 @@ impl<'a> SitePackages<'a> { let metadata = distribution .metadata() .with_context(|| format!("Failed to read metadata for: {distribution}"))?; - requirements.extend(metadata.requires_dist); + + // Add the dependencies to the queue. + for dependency in metadata.requires_dist { + if dependency + .evaluate_markers(self.venv.interpreter().markers(), &requirement.extras) + { + if seen.insert(dependency.clone()) { + stack.push(dependency); + } + } + } } // Verify that all non-editable requirements are met. - while let Some(requirement) = requirements.pop() { - if !requirement.evaluate_markers(self.venv.interpreter().markers(), &[]) { - continue; - } - + while let Some(requirement) = stack.pop() { let Some(distribution) = self .by_name .get(&requirement.name) @@ -300,11 +315,19 @@ impl<'a> SitePackages<'a> { } // Recurse into the dependencies. - if seen.insert(requirement) { - let metadata = distribution - .metadata() - .with_context(|| format!("Failed to read metadata for: {distribution}"))?; - requirements.extend(metadata.requires_dist); + let metadata = distribution + .metadata() + .with_context(|| format!("Failed to read metadata for: {distribution}"))?; + + // Add the dependencies to the queue. + for dependency in metadata.requires_dist { + if dependency + .evaluate_markers(self.venv.interpreter().markers(), &requirement.extras) + { + if seen.insert(dependency.clone()) { + stack.push(dependency); + } + } } } diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 077277bca..11ae2289a 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -317,6 +317,65 @@ fn respect_installed_and_reinstall() -> Result<()> { Ok(()) } +/// Respect installed versions when resolving. +#[test] +#[cfg(not(windows))] +fn reinstall_extras() -> Result<()> { + let context = TestContext::new("3.12"); + + // Install anyio. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio")?; + + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.0.0 + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + context.assert_command("import anyio").success(); + + // Re-install anyio, with an extra. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("anyio[trio]")?; + + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 7 packages in [TIME] + Downloaded 4 packages in [TIME] + Installed 4 packages in [TIME] + + attrs==23.1.0 + + outcome==1.3.0.post0 + + sortedcontainers==2.4.0 + + trio==0.23.1 + "### + ); + + context.assert_command("import anyio").success(); + + Ok(()) +} + /// Like `pip`, we (unfortunately) allow incompatible environments. #[test] fn allow_incompatibilities() -> Result<()> {