From 6d32ab671f1a525e8cd046bc4654bcf864a1a985 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 21 Jun 2025 22:07:48 -0400 Subject: [PATCH] Support conflicting editable settings across groups --- crates/uv-distribution-types/src/buildable.rs | 7 +- crates/uv-distribution-types/src/lib.rs | 12 +- .../uv-distribution-types/src/requirement.rs | 32 +- .../src/index/built_wheel_index.rs | 2 +- .../uv-distribution/src/metadata/lowering.rs | 20 +- crates/uv-distribution/src/source/mod.rs | 4 +- crates/uv-installer/src/satisfies.rs | 2 +- crates/uv-pypi-types/src/parsed_url.rs | 29 +- crates/uv-requirements-txt/src/lib.rs | 6 +- crates/uv-requirements-txt/src/requirement.rs | 4 +- ...ts_txt__test__parse-unix-bare-url.txt.snap | 24 +- ...ts_txt__test__parse-unix-editable.txt.snap | 48 ++- ...txt__test__parse-windows-bare-url.txt.snap | 24 +- ...txt__test__parse-windows-editable.txt.snap | 48 ++- crates/uv-requirements/src/source_tree.rs | 2 +- .../src/lock/export/pylock_toml.rs | 8 +- crates/uv-resolver/src/lock/mod.rs | 20 +- crates/uv-resolver/src/resolver/mod.rs | 1 + crates/uv-resolver/src/resolver/urls.rs | 9 +- crates/uv-workspace/src/workspace.rs | 16 +- crates/uv/src/commands/project/sync.rs | 10 +- crates/uv/tests/it/lock.rs | 86 +---- crates/uv/tests/it/sync.rs | 340 ++++++++++++++++++ 23 files changed, 538 insertions(+), 216 deletions(-) diff --git a/crates/uv-distribution-types/src/buildable.rs b/crates/uv-distribution-types/src/buildable.rs index c97bb362f..75997e406 100644 --- a/crates/uv-distribution-types/src/buildable.rs +++ b/crates/uv-distribution-types/src/buildable.rs @@ -124,7 +124,10 @@ impl SourceUrl<'_> { pub fn is_editable(&self) -> bool { matches!( self, - Self::Directory(DirectorySourceUrl { editable: true, .. }) + Self::Directory(DirectorySourceUrl { + editable: Some(true), + .. + }) ) } @@ -210,7 +213,7 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> { pub struct DirectorySourceUrl<'a> { pub url: &'a DisplaySafeUrl, pub install_path: Cow<'a, Path>, - pub editable: bool, + pub editable: Option, } impl std::fmt::Display for DirectorySourceUrl<'_> { diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index 1e3ad7eba..0b25669b0 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -343,9 +343,9 @@ pub struct DirectorySourceDist { /// The absolute path to the distribution which we use for installing. pub install_path: Box, /// Whether the package should be installed in editable mode. - pub editable: bool, + pub editable: Option, /// Whether the package should be built and installed. - pub r#virtual: bool, + pub r#virtual: Option, /// The URL as it was provided by the user. pub url: VerbatimUrl, } @@ -452,8 +452,8 @@ impl Dist { name: PackageName, url: VerbatimUrl, install_path: &Path, - editable: bool, - r#virtual: bool, + editable: Option, + r#virtual: Option, ) -> Result { // Convert to an absolute path. let install_path = path::absolute(install_path)?; @@ -655,7 +655,7 @@ impl SourceDist { /// Returns `true` if the distribution is editable. pub fn is_editable(&self) -> bool { match self { - Self::Directory(DirectorySourceDist { editable, .. }) => *editable, + Self::Directory(DirectorySourceDist { editable, .. }) => editable.unwrap_or(false), _ => false, } } @@ -663,7 +663,7 @@ impl SourceDist { /// Returns `true` if the distribution is virtual. pub fn is_virtual(&self) -> bool { match self { - Self::Directory(DirectorySourceDist { r#virtual, .. }) => *r#virtual, + Self::Directory(DirectorySourceDist { r#virtual, .. }) => r#virtual.unwrap_or(false), _ => false, } } diff --git a/crates/uv-distribution-types/src/requirement.rs b/crates/uv-distribution-types/src/requirement.rs index 432cc4e12..104cf396c 100644 --- a/crates/uv-distribution-types/src/requirement.rs +++ b/crates/uv-distribution-types/src/requirement.rs @@ -429,9 +429,9 @@ pub enum RequirementSource { /// The absolute path to the distribution which we use for installing. install_path: Box, /// For a source tree (a directory), whether to install as an editable. - editable: bool, + editable: Option, /// For a source tree (a directory), whether the project should be built and installed. - r#virtual: bool, + r#virtual: Option, /// The PEP 508 style URL in the format /// `file:///#subdirectory=`. url: VerbatimUrl, @@ -545,7 +545,13 @@ impl RequirementSource { /// Returns `true` if the source is editable. pub fn is_editable(&self) -> bool { - matches!(self, Self::Directory { editable: true, .. }) + matches!( + self, + Self::Directory { + editable: Some(true), + .. + } + ) } /// Returns `true` if the source is empty. @@ -792,11 +798,11 @@ impl From for RequirementSourceWire { r#virtual, url: _, } => { - if editable { + if editable.unwrap_or(false) { Self::Editable { editable: PortablePathBuf::from(install_path), } - } else if r#virtual { + } else if r#virtual.unwrap_or(false) { Self::Virtual { r#virtual: PortablePathBuf::from(install_path), } @@ -908,8 +914,8 @@ impl TryFrom for RequirementSource { ))?; Ok(Self::Directory { install_path: directory, - editable: false, - r#virtual: false, + editable: Some(false), + r#virtual: Some(false), url, }) } @@ -920,8 +926,8 @@ impl TryFrom for RequirementSource { ))?; Ok(Self::Directory { install_path: editable, - editable: true, - r#virtual: false, + editable: Some(true), + r#virtual: Some(false), url, }) } @@ -932,8 +938,8 @@ impl TryFrom for RequirementSource { ))?; Ok(Self::Directory { install_path: r#virtual, - editable: false, - r#virtual: true, + editable: Some(false), + r#virtual: Some(true), url, }) } @@ -980,8 +986,8 @@ mod tests { marker: MarkerTree::TRUE, source: RequirementSource::Directory { install_path: PathBuf::from(path).into_boxed_path(), - editable: false, - r#virtual: false, + editable: Some(false), + r#virtual: Some(false), url: VerbatimUrl::from_absolute_path(path).unwrap(), }, origin: None, diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index fb376d1b4..9752e7e4f 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -119,7 +119,7 @@ impl<'a> BuiltWheelIndex<'a> { ) -> Result, Error> { let cache_shard = self.cache.shard( CacheBucket::SourceDistributions, - if source_dist.editable { + if source_dist.editable.unwrap_or(false) { WheelCache::Editable(&source_dist.url).root() } else { WheelCache::Path(&source_dist.url).root() diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 330075842..54782c083 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -310,15 +310,15 @@ impl LoweredRequirement { RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, - editable: true, - r#virtual: false, + editable: Some(true), + r#virtual: Some(false), } } else { RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, - editable: false, - r#virtual: true, + editable: Some(false), + r#virtual: Some(true), } }; (source, marker) @@ -724,8 +724,8 @@ fn path_source( Ok(RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, - editable: true, - r#virtual: false, + editable, + r#virtual: Some(false), }) } else { // Determine whether the project is a package or virtual. @@ -738,12 +738,14 @@ fn path_source( .unwrap_or(true) }); + // If the project is not a package, treat it as a virtual dependency. + let r#virtual = !is_package; + Ok(RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, - editable: false, - // If a project is not a package, treat it as a virtual dependency. - r#virtual: !is_package, + editable: Some(false), + r#virtual: Some(r#virtual), }) } } else { diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 2b73eb4ff..544b7bc9a 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1070,7 +1070,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, - if resource.editable { + if resource.editable.unwrap_or(false) { WheelCache::Editable(resource.url).root() } else { WheelCache::Path(resource.url).root() @@ -1183,7 +1183,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, - if resource.editable { + if resource.editable.unwrap_or(false) { WheelCache::Editable(resource.url).root() } else { WheelCache::Path(resource.url).root() diff --git a/crates/uv-installer/src/satisfies.rs b/crates/uv-installer/src/satisfies.rs index a91676595..b7e824202 100644 --- a/crates/uv-installer/src/satisfies.rs +++ b/crates/uv-installer/src/satisfies.rs @@ -241,7 +241,7 @@ impl RequirementSatisfaction { return Self::Mismatch; }; - if *requested_editable != installed_editable.unwrap_or_default() { + if requested_editable != installed_editable { trace!( "Editable mismatch: {:?} vs. {:?}", *requested_editable, diff --git a/crates/uv-pypi-types/src/parsed_url.rs b/crates/uv-pypi-types/src/parsed_url.rs index 9517dfdc6..57afbcdf9 100644 --- a/crates/uv-pypi-types/src/parsed_url.rs +++ b/crates/uv-pypi-types/src/parsed_url.rs @@ -86,8 +86,8 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { ParsedUrl::Directory(ParsedDirectoryUrl { url, install_path, - editable: false, - r#virtual: false, + editable: None, + r#virtual: None, }) } else { ParsedUrl::Path(ParsedPathUrl { @@ -118,8 +118,8 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { ParsedUrl::Directory(ParsedDirectoryUrl { url, install_path, - editable: false, - r#virtual: false, + editable: None, + r#virtual: None, }) } else { ParsedUrl::Path(ParsedPathUrl { @@ -187,7 +187,10 @@ impl ParsedUrl { pub fn is_editable(&self) -> bool { matches!( self, - Self::Directory(ParsedDirectoryUrl { editable: true, .. }) + Self::Directory(ParsedDirectoryUrl { + editable: Some(true), + .. + }) ) } } @@ -226,16 +229,18 @@ pub struct ParsedDirectoryUrl { pub url: DisplaySafeUrl, /// The absolute path to the distribution which we use for installing. pub install_path: Box, - pub editable: bool, - pub r#virtual: bool, + /// Whether the project at the given URL should be installed in editable mode. + pub editable: Option, + /// Whether the project at the given URL should be treated as a virtual package. + pub r#virtual: Option, } impl ParsedDirectoryUrl { /// Construct a [`ParsedDirectoryUrl`] from a path requirement source. pub fn from_source( install_path: Box, - editable: bool, - r#virtual: bool, + editable: Option, + r#virtual: Option, url: DisplaySafeUrl, ) -> Self { Self { @@ -399,8 +404,8 @@ impl TryFrom for ParsedUrl { Ok(Self::Directory(ParsedDirectoryUrl { url, install_path: path.into_boxed_path(), - editable: false, - r#virtual: false, + editable: None, + r#virtual: None, })) } else { Ok(Self::Path(ParsedPathUrl { @@ -445,7 +450,7 @@ impl From<&ParsedDirectoryUrl> for DirectUrl { Self::LocalDirectory { url: value.url.to_string(), dir_info: DirInfo { - editable: value.editable.then_some(true), + editable: value.editable, }, subdirectory: None, } diff --git a/crates/uv-requirements-txt/src/lib.rs b/crates/uv-requirements-txt/src/lib.rs index b734bf8a2..b95875768 100644 --- a/crates/uv-requirements-txt/src/lib.rs +++ b/crates/uv-requirements-txt/src/lib.rs @@ -2064,8 +2064,10 @@ mod test { fragment: None, }, install_path: "/foo/bar", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { diff --git a/crates/uv-requirements-txt/src/requirement.rs b/crates/uv-requirements-txt/src/requirement.rs index 285753ed8..6c7cf0b52 100644 --- a/crates/uv-requirements-txt/src/requirement.rs +++ b/crates/uv-requirements-txt/src/requirement.rs @@ -90,7 +90,7 @@ impl RequirementsTxtRequirement { version_or_url: Some(uv_pep508::VersionOrUrl::Url(VerbatimParsedUrl { verbatim: url.verbatim, parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { - editable: true, + editable: Some(true), ..parsed_url }), })), @@ -115,7 +115,7 @@ impl RequirementsTxtRequirement { url: VerbatimParsedUrl { verbatim: requirement.url.verbatim, parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { - editable: true, + editable: Some(true), ..parsed_url }), }, diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap index f2187a1a2..dd03d09bf 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap @@ -22,8 +22,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -72,8 +72,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -126,8 +126,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -176,8 +176,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -226,8 +226,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -276,8 +276,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap index 222ab6b10..39a4885dc 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap @@ -24,8 +24,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -81,8 +83,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -138,8 +142,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -195,8 +201,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -252,8 +260,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -302,8 +312,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable[d", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -352,8 +364,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -402,8 +416,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap index 72e1c8635..be90c5c44 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap @@ -22,8 +22,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -72,8 +72,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -126,8 +126,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -176,8 +176,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -226,8 +226,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { @@ -276,8 +276,8 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black editable", - editable: false, - virtual: false, + editable: None, + virtual: None, }, ), verbatim: VerbatimUrl { diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap index 84ae22816..dde16b40c 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap @@ -24,8 +24,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -81,8 +83,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -138,8 +142,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -195,8 +201,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -252,8 +260,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -302,8 +312,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable[d", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -352,8 +364,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { @@ -402,8 +416,10 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - editable: true, - virtual: false, + editable: Some( + true, + ), + virtual: None, }, ), verbatim: VerbatimUrl { diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index 39fbe453b..a7a99c5a2 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -154,7 +154,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { let source = SourceUrl::Directory(DirectorySourceUrl { url: &url, install_path: Cow::Borrowed(source_tree), - editable: false, + editable: None, }); // Determine the hash policy. Since we don't have a package name, we perform a diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index d2c2383a5..74d874178 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -500,7 +500,7 @@ impl<'lock> PylockToml { .unwrap_or_else(|_| dist.install_path.clone()); package.directory = Some(PylockTomlDirectory { path: PortablePathBuf::from(path), - editable: if dist.editable { Some(true) } else { None }, + editable: dist.editable, subdirectory: None, }); } @@ -737,7 +737,7 @@ impl<'lock> PylockToml { ), editable: match editable { EditableMode::NonEditable => None, - EditableMode::Editable => Some(sdist.editable), + EditableMode::Editable => sdist.editable, }, subdirectory: None, }), @@ -1394,8 +1394,8 @@ impl PylockTomlDirectory { Ok(DirectorySourceDist { name: name.clone(), install_path: path.into_boxed_path(), - editable: self.editable.unwrap_or(false), - r#virtual: false, + editable: self.editable, + r#virtual: Some(false), url, }) } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index beeadc912..1aac4e6b6 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2402,8 +2402,8 @@ impl Package { name: self.id.name.clone(), url: verbatim_url(&install_path, &self.id)?, install_path: install_path.into_boxed_path(), - editable: false, - r#virtual: false, + editable: Some(false), + r#virtual: Some(false), }; uv_distribution_types::SourceDist::Directory(dir_dist) } @@ -2413,8 +2413,8 @@ impl Package { name: self.id.name.clone(), url: verbatim_url(&install_path, &self.id)?, install_path: install_path.into_boxed_path(), - editable: true, - r#virtual: false, + editable: Some(true), + r#virtual: Some(false), }; uv_distribution_types::SourceDist::Directory(dir_dist) } @@ -2424,8 +2424,8 @@ impl Package { name: self.id.name.clone(), url: verbatim_url(&install_path, &self.id)?, install_path: install_path.into_boxed_path(), - editable: false, - r#virtual: true, + editable: Some(false), + r#virtual: Some(true), }; uv_distribution_types::SourceDist::Directory(dir_dist) } @@ -3256,9 +3256,9 @@ impl Source { let path = relative_to(&directory_dist.install_path, root) .or_else(|_| std::path::absolute(&directory_dist.install_path)) .map_err(LockErrorKind::DistributionRelativePath)?; - if directory_dist.editable { + if directory_dist.editable.unwrap_or(false) { Ok(Source::Editable(path.into_boxed_path())) - } else if directory_dist.r#virtual { + } else if directory_dist.r#virtual.unwrap_or(false) { Ok(Source::Virtual(path.into_boxed_path())) } else { Ok(Source::Directory(path.into_boxed_path())) @@ -4806,8 +4806,8 @@ fn normalize_requirement( marker: requires_python.simplify_markers(requirement.marker), source: RequirementSource::Directory { install_path, - editable, - r#virtual, + editable: Some(editable.unwrap_or(false)), + r#virtual: Some(r#virtual.unwrap_or(false)), url, }, origin: None, diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index ed1cd48af..c86b32a01 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -619,6 +619,7 @@ impl ResolverState { // Then here, if we get a reason that we consider unrecoverable, we should diff --git a/crates/uv-resolver/src/resolver/urls.rs b/crates/uv-resolver/src/resolver/urls.rs index 73d190b4a..57803ed0b 100644 --- a/crates/uv-resolver/src/resolver/urls.rs +++ b/crates/uv-resolver/src/resolver/urls.rs @@ -63,9 +63,9 @@ impl Urls { verbatim: _, } = package_url { - if !*editable { + if editable.is_none() { debug!("Allowing an editable variant of {}", &package_url.verbatim); - *editable = true; + *editable = Some(true); } } } @@ -201,8 +201,9 @@ fn same_resource(a: &ParsedUrl, b: &ParsedUrl, git: &GitResolver) -> bool { || is_same_file(&a.install_path, &b.install_path).unwrap_or(false) } (ParsedUrl::Directory(a), ParsedUrl::Directory(b)) => { - a.install_path == b.install_path - || is_same_file(&a.install_path, &b.install_path).unwrap_or(false) + (a.install_path == b.install_path + || is_same_file(&a.install_path, &b.install_path).unwrap_or(false)) + && a.editable.is_none_or(|a| b.editable.is_none_or(|b| a == b)) } _ => false, } diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 1349d739c..8d09554d9 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -315,15 +315,15 @@ impl Workspace { source: if member.pyproject_toml.is_package() { RequirementSource::Directory { install_path: member.root.clone().into_boxed_path(), - editable: true, - r#virtual: false, + editable: Some(true), + r#virtual: Some(false), url, } } else { RequirementSource::Directory { install_path: member.root.clone().into_boxed_path(), - editable: false, - r#virtual: true, + editable: Some(false), + r#virtual: Some(true), url, } }, @@ -371,15 +371,15 @@ impl Workspace { source: if member.pyproject_toml.is_package() { RequirementSource::Directory { install_path: member.root.clone().into_boxed_path(), - editable: true, - r#virtual: false, + editable: Some(true), + r#virtual: Some(false), url, } } else { RequirementSource::Directory { install_path: member.root.clone().into_boxed_path(), - editable: false, - r#virtual: true, + editable: Some(false), + r#virtual: Some(true), url, } }, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 19848ee02..36e25dde3 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -802,7 +802,7 @@ fn apply_no_virtual_project(resolution: Resolution) -> Resolution { return true; }; - !dist.r#virtual + !dist.r#virtual.unwrap_or(false) }) } @@ -820,8 +820,8 @@ fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolu let Dist::Source(SourceDist::Directory(DirectorySourceDist { name, install_path, - editable: true, - r#virtual: false, + editable: Some(true), + r#virtual, url, })) = dist.as_ref() else { @@ -832,8 +832,8 @@ fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolu dist: Arc::new(Dist::Source(SourceDist::Directory(DirectorySourceDist { name: name.clone(), install_path: install_path.clone(), - editable: false, - r#virtual: false, + editable: Some(false), + r#virtual: *r#virtual, url: url.clone(), }))), version: version.clone(), diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 1c375b2e3..0e55b179a 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -10944,7 +10944,7 @@ fn lock_sources_source_tree() -> Result<()> { } /// Lock a project in which a given dependency is requested from two different members, once as -/// editable, and once as non-editable. +/// editable, and once as non-editable. This should trigger a conflicting URL error. #[test] fn lock_editable() -> Result<()> { let context = TestContext::new("3.12"); @@ -11084,86 +11084,16 @@ fn lock_editable() -> Result<()> { library = { path = "../../library", editable = true } "#})?; - uv_snapshot!(context.filters(), context.lock(), @r###" - success: true - exit_code: 0 + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] - "###); - - let lock = context.read("uv.lock"); - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r#" - version = 1 - revision = 2 - requires-python = ">=3.12" - - [options] - exclude-newer = "2024-03-25T00:00:00Z" - - [manifest] - members = [ - "leaf", - "workspace", - ] - - [[package]] - name = "leaf" - version = "0.1.0" - source = { editable = "packages/leaf" } - dependencies = [ - { name = "library" }, - ] - - [package.metadata] - requires-dist = [{ name = "library", editable = "library" }] - - [[package]] - name = "library" - version = "0.1.0" - source = { editable = "library" } - - [[package]] - name = "workspace" - version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "library" }, - ] - - [package.metadata] - requires-dist = [{ name = "library", directory = "library" }] - "# - ); - }); - - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - "###); - - // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + library==0.1.0 (from file://[TEMP_DIR]/library) - "###); + error: Requirements contain conflicting URLs for package `library` in all marker environments: + - file://[TEMP_DIR]/library + - file://[TEMP_DIR]/library (editable) + "); Ok(()) } diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index da59682ab..deaf0f9fc 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9989,3 +9989,343 @@ fn sync_url_with_query_parameters() -> Result<()> { Ok(()) } + +/// See: +#[test] +#[cfg(not(windows))] +fn conflicting_editable() -> 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 = [] + + [dependency-groups] + foo = [ + "child", + ] + bar = [ + "child", + ] + + [tool.uv] + conflicts = [ + [ + { group = "foo" }, + { group = "bar" }, + ], + ] + + [tool.uv.sources] + child = [ + { path = "./child", editable = true, group = "foo" }, + { path = "./child", editable = false, group = "bar" }, + ] + "#, + )?; + + context + .temp_dir + .child("child") + .child("pyproject.toml") + .write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + context + .temp_dir + .child("child") + .child("src") + .child("child") + .child("__init__.py") + .touch()?; + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Audited in [TIME] + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + conflicts = [[ + { package = "project", group = "bar" }, + { package = "project", group = "foo" }, + ]] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "child" + version = "0.1.0" + source = { directory = "child" } + + [[package]] + name = "child" + version = "0.1.0" + source = { editable = "child" } + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + + [package.dev-dependencies] + bar = [ + { name = "child", version = "0.1.0", source = { directory = "child" } }, + ] + foo = [ + { name = "child", version = "0.1.0", source = { editable = "child" } }, + ] + + [package.metadata] + + [package.metadata.requires-dev] + bar = [{ name = "child", directory = "child" }] + foo = [{ name = "child", editable = "child" }] + "# + ); + }); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + uv_snapshot!(context.filters(), context.pip_list().arg("--format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + [{"name":"child","version":"0.1.0","editable_project_location":"[TEMP_DIR]/child"}] + + ----- stderr ----- + "#); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bar"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + ~ child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + uv_snapshot!(context.filters(), context.pip_list().arg("--format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + [{"name":"child","version":"0.1.0"}] + + ----- stderr ----- + "#); + + Ok(()) +} + +/// See: +#[test] +#[cfg(not(windows))] +fn undeclared_editable() -> 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 = [] + + [dependency-groups] + foo = [ + "child", + ] + bar = [ + "child", + ] + + [tool.uv] + conflicts = [ + [ + { group = "foo" }, + { group = "bar" }, + ], + ] + + [tool.uv.sources] + child = [ + { path = "./child", editable = true, group = "foo" }, + { path = "./child", group = "bar" }, + ] + "#, + )?; + + context + .temp_dir + .child("child") + .child("pyproject.toml") + .write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + context + .temp_dir + .child("child") + .child("src") + .child("child") + .child("__init__.py") + .touch()?; + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Audited in [TIME] + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + conflicts = [[ + { package = "project", group = "bar" }, + { package = "project", group = "foo" }, + ]] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "child" + version = "0.1.0" + source = { directory = "child" } + + [[package]] + name = "child" + version = "0.1.0" + source = { editable = "child" } + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + + [package.dev-dependencies] + bar = [ + { name = "child", version = "0.1.0", source = { directory = "child" } }, + ] + foo = [ + { name = "child", version = "0.1.0", source = { editable = "child" } }, + ] + + [package.metadata] + + [package.metadata.requires-dev] + bar = [{ name = "child", directory = "child" }] + foo = [{ name = "child", editable = "child" }] + "# + ); + }); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + uv_snapshot!(context.filters(), context.pip_list().arg("--format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + [{"name":"child","version":"0.1.0","editable_project_location":"[TEMP_DIR]/child"}] + + ----- stderr ----- + "#); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bar"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + ~ child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + uv_snapshot!(context.filters(), context.pip_list().arg("--format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + [{"name":"child","version":"0.1.0"}] + + ----- stderr ----- + "#); + + Ok(()) +}