Support conflicting editable settings across groups

This commit is contained in:
Charlie Marsh 2025-06-21 22:07:48 -04:00
parent e104eea34a
commit 6d32ab671f
23 changed files with 538 additions and 216 deletions

View file

@ -124,7 +124,10 @@ impl SourceUrl<'_> {
pub fn is_editable(&self) -> bool { pub fn is_editable(&self) -> bool {
matches!( matches!(
self, 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 struct DirectorySourceUrl<'a> {
pub url: &'a DisplaySafeUrl, pub url: &'a DisplaySafeUrl,
pub install_path: Cow<'a, Path>, pub install_path: Cow<'a, Path>,
pub editable: bool, pub editable: Option<bool>,
} }
impl std::fmt::Display for DirectorySourceUrl<'_> { impl std::fmt::Display for DirectorySourceUrl<'_> {

View file

@ -343,9 +343,9 @@ pub struct DirectorySourceDist {
/// The absolute path to the distribution which we use for installing. /// The absolute path to the distribution which we use for installing.
pub install_path: Box<Path>, pub install_path: Box<Path>,
/// Whether the package should be installed in editable mode. /// Whether the package should be installed in editable mode.
pub editable: bool, pub editable: Option<bool>,
/// Whether the package should be built and installed. /// Whether the package should be built and installed.
pub r#virtual: bool, pub r#virtual: Option<bool>,
/// The URL as it was provided by the user. /// The URL as it was provided by the user.
pub url: VerbatimUrl, pub url: VerbatimUrl,
} }
@ -452,8 +452,8 @@ impl Dist {
name: PackageName, name: PackageName,
url: VerbatimUrl, url: VerbatimUrl,
install_path: &Path, install_path: &Path,
editable: bool, editable: Option<bool>,
r#virtual: bool, r#virtual: Option<bool>,
) -> Result<Dist, Error> { ) -> Result<Dist, Error> {
// Convert to an absolute path. // Convert to an absolute path.
let install_path = path::absolute(install_path)?; let install_path = path::absolute(install_path)?;
@ -655,7 +655,7 @@ impl SourceDist {
/// Returns `true` if the distribution is editable. /// Returns `true` if the distribution is editable.
pub fn is_editable(&self) -> bool { pub fn is_editable(&self) -> bool {
match self { match self {
Self::Directory(DirectorySourceDist { editable, .. }) => *editable, Self::Directory(DirectorySourceDist { editable, .. }) => editable.unwrap_or(false),
_ => false, _ => false,
} }
} }
@ -663,7 +663,7 @@ impl SourceDist {
/// Returns `true` if the distribution is virtual. /// Returns `true` if the distribution is virtual.
pub fn is_virtual(&self) -> bool { pub fn is_virtual(&self) -> bool {
match self { match self {
Self::Directory(DirectorySourceDist { r#virtual, .. }) => *r#virtual, Self::Directory(DirectorySourceDist { r#virtual, .. }) => r#virtual.unwrap_or(false),
_ => false, _ => false,
} }
} }

View file

@ -429,9 +429,9 @@ pub enum RequirementSource {
/// The absolute path to the distribution which we use for installing. /// The absolute path to the distribution which we use for installing.
install_path: Box<Path>, install_path: Box<Path>,
/// For a source tree (a directory), whether to install as an editable. /// For a source tree (a directory), whether to install as an editable.
editable: bool, editable: Option<bool>,
/// For a source tree (a directory), whether the project should be built and installed. /// For a source tree (a directory), whether the project should be built and installed.
r#virtual: bool, r#virtual: Option<bool>,
/// The PEP 508 style URL in the format /// The PEP 508 style URL in the format
/// `file:///<path>#subdirectory=<subdirectory>`. /// `file:///<path>#subdirectory=<subdirectory>`.
url: VerbatimUrl, url: VerbatimUrl,
@ -545,7 +545,13 @@ impl RequirementSource {
/// Returns `true` if the source is editable. /// Returns `true` if the source is editable.
pub fn is_editable(&self) -> bool { 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. /// Returns `true` if the source is empty.
@ -792,11 +798,11 @@ impl From<RequirementSource> for RequirementSourceWire {
r#virtual, r#virtual,
url: _, url: _,
} => { } => {
if editable { if editable.unwrap_or(false) {
Self::Editable { Self::Editable {
editable: PortablePathBuf::from(install_path), editable: PortablePathBuf::from(install_path),
} }
} else if r#virtual { } else if r#virtual.unwrap_or(false) {
Self::Virtual { Self::Virtual {
r#virtual: PortablePathBuf::from(install_path), r#virtual: PortablePathBuf::from(install_path),
} }
@ -908,8 +914,8 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
))?; ))?;
Ok(Self::Directory { Ok(Self::Directory {
install_path: directory, install_path: directory,
editable: false, editable: Some(false),
r#virtual: false, r#virtual: Some(false),
url, url,
}) })
} }
@ -920,8 +926,8 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
))?; ))?;
Ok(Self::Directory { Ok(Self::Directory {
install_path: editable, install_path: editable,
editable: true, editable: Some(true),
r#virtual: false, r#virtual: Some(false),
url, url,
}) })
} }
@ -932,8 +938,8 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
))?; ))?;
Ok(Self::Directory { Ok(Self::Directory {
install_path: r#virtual, install_path: r#virtual,
editable: false, editable: Some(false),
r#virtual: true, r#virtual: Some(true),
url, url,
}) })
} }
@ -980,8 +986,8 @@ mod tests {
marker: MarkerTree::TRUE, marker: MarkerTree::TRUE,
source: RequirementSource::Directory { source: RequirementSource::Directory {
install_path: PathBuf::from(path).into_boxed_path(), install_path: PathBuf::from(path).into_boxed_path(),
editable: false, editable: Some(false),
r#virtual: false, r#virtual: Some(false),
url: VerbatimUrl::from_absolute_path(path).unwrap(), url: VerbatimUrl::from_absolute_path(path).unwrap(),
}, },
origin: None, origin: None,

View file

@ -119,7 +119,7 @@ impl<'a> BuiltWheelIndex<'a> {
) -> Result<Option<CachedWheel>, Error> { ) -> Result<Option<CachedWheel>, Error> {
let cache_shard = self.cache.shard( let cache_shard = self.cache.shard(
CacheBucket::SourceDistributions, CacheBucket::SourceDistributions,
if source_dist.editable { if source_dist.editable.unwrap_or(false) {
WheelCache::Editable(&source_dist.url).root() WheelCache::Editable(&source_dist.url).root()
} else { } else {
WheelCache::Path(&source_dist.url).root() WheelCache::Path(&source_dist.url).root()

View file

@ -310,15 +310,15 @@ impl LoweredRequirement {
RequirementSource::Directory { RequirementSource::Directory {
install_path: install_path.into_boxed_path(), install_path: install_path.into_boxed_path(),
url, url,
editable: true, editable: Some(true),
r#virtual: false, r#virtual: Some(false),
} }
} else { } else {
RequirementSource::Directory { RequirementSource::Directory {
install_path: install_path.into_boxed_path(), install_path: install_path.into_boxed_path(),
url, url,
editable: false, editable: Some(false),
r#virtual: true, r#virtual: Some(true),
} }
}; };
(source, marker) (source, marker)
@ -724,8 +724,8 @@ fn path_source(
Ok(RequirementSource::Directory { Ok(RequirementSource::Directory {
install_path: install_path.into_boxed_path(), install_path: install_path.into_boxed_path(),
url, url,
editable: true, editable,
r#virtual: false, r#virtual: Some(false),
}) })
} else { } else {
// Determine whether the project is a package or virtual. // Determine whether the project is a package or virtual.
@ -738,12 +738,14 @@ fn path_source(
.unwrap_or(true) .unwrap_or(true)
}); });
// If the project is not a package, treat it as a virtual dependency.
let r#virtual = !is_package;
Ok(RequirementSource::Directory { Ok(RequirementSource::Directory {
install_path: install_path.into_boxed_path(), install_path: install_path.into_boxed_path(),
url, url,
editable: false, editable: Some(false),
// If a project is not a package, treat it as a virtual dependency. r#virtual: Some(r#virtual),
r#virtual: !is_package,
}) })
} }
} else { } else {

View file

@ -1070,7 +1070,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = self.build_context.cache().shard( let cache_shard = self.build_context.cache().shard(
CacheBucket::SourceDistributions, CacheBucket::SourceDistributions,
if resource.editable { if resource.editable.unwrap_or(false) {
WheelCache::Editable(resource.url).root() WheelCache::Editable(resource.url).root()
} else { } else {
WheelCache::Path(resource.url).root() WheelCache::Path(resource.url).root()
@ -1183,7 +1183,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = self.build_context.cache().shard( let cache_shard = self.build_context.cache().shard(
CacheBucket::SourceDistributions, CacheBucket::SourceDistributions,
if resource.editable { if resource.editable.unwrap_or(false) {
WheelCache::Editable(resource.url).root() WheelCache::Editable(resource.url).root()
} else { } else {
WheelCache::Path(resource.url).root() WheelCache::Path(resource.url).root()

View file

@ -241,7 +241,7 @@ impl RequirementSatisfaction {
return Self::Mismatch; return Self::Mismatch;
}; };
if *requested_editable != installed_editable.unwrap_or_default() { if requested_editable != installed_editable {
trace!( trace!(
"Editable mismatch: {:?} vs. {:?}", "Editable mismatch: {:?} vs. {:?}",
*requested_editable, *requested_editable,

View file

@ -86,8 +86,8 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
ParsedUrl::Directory(ParsedDirectoryUrl { ParsedUrl::Directory(ParsedDirectoryUrl {
url, url,
install_path, install_path,
editable: false, editable: None,
r#virtual: false, r#virtual: None,
}) })
} else { } else {
ParsedUrl::Path(ParsedPathUrl { ParsedUrl::Path(ParsedPathUrl {
@ -118,8 +118,8 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
ParsedUrl::Directory(ParsedDirectoryUrl { ParsedUrl::Directory(ParsedDirectoryUrl {
url, url,
install_path, install_path,
editable: false, editable: None,
r#virtual: false, r#virtual: None,
}) })
} else { } else {
ParsedUrl::Path(ParsedPathUrl { ParsedUrl::Path(ParsedPathUrl {
@ -187,7 +187,10 @@ impl ParsedUrl {
pub fn is_editable(&self) -> bool { pub fn is_editable(&self) -> bool {
matches!( matches!(
self, self,
Self::Directory(ParsedDirectoryUrl { editable: true, .. }) Self::Directory(ParsedDirectoryUrl {
editable: Some(true),
..
})
) )
} }
} }
@ -226,16 +229,18 @@ pub struct ParsedDirectoryUrl {
pub url: DisplaySafeUrl, pub url: DisplaySafeUrl,
/// The absolute path to the distribution which we use for installing. /// The absolute path to the distribution which we use for installing.
pub install_path: Box<Path>, pub install_path: Box<Path>,
pub editable: bool, /// Whether the project at the given URL should be installed in editable mode.
pub r#virtual: bool, pub editable: Option<bool>,
/// Whether the project at the given URL should be treated as a virtual package.
pub r#virtual: Option<bool>,
} }
impl ParsedDirectoryUrl { impl ParsedDirectoryUrl {
/// Construct a [`ParsedDirectoryUrl`] from a path requirement source. /// Construct a [`ParsedDirectoryUrl`] from a path requirement source.
pub fn from_source( pub fn from_source(
install_path: Box<Path>, install_path: Box<Path>,
editable: bool, editable: Option<bool>,
r#virtual: bool, r#virtual: Option<bool>,
url: DisplaySafeUrl, url: DisplaySafeUrl,
) -> Self { ) -> Self {
Self { Self {
@ -399,8 +404,8 @@ impl TryFrom<DisplaySafeUrl> for ParsedUrl {
Ok(Self::Directory(ParsedDirectoryUrl { Ok(Self::Directory(ParsedDirectoryUrl {
url, url,
install_path: path.into_boxed_path(), install_path: path.into_boxed_path(),
editable: false, editable: None,
r#virtual: false, r#virtual: None,
})) }))
} else { } else {
Ok(Self::Path(ParsedPathUrl { Ok(Self::Path(ParsedPathUrl {
@ -445,7 +450,7 @@ impl From<&ParsedDirectoryUrl> for DirectUrl {
Self::LocalDirectory { Self::LocalDirectory {
url: value.url.to_string(), url: value.url.to_string(),
dir_info: DirInfo { dir_info: DirInfo {
editable: value.editable.then_some(true), editable: value.editable,
}, },
subdirectory: None, subdirectory: None,
} }

View file

@ -2064,8 +2064,10 @@ mod test {
fragment: None, fragment: None,
}, },
install_path: "/foo/bar", install_path: "/foo/bar",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {

View file

@ -90,7 +90,7 @@ impl RequirementsTxtRequirement {
version_or_url: Some(uv_pep508::VersionOrUrl::Url(VerbatimParsedUrl { version_or_url: Some(uv_pep508::VersionOrUrl::Url(VerbatimParsedUrl {
verbatim: url.verbatim, verbatim: url.verbatim,
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
editable: true, editable: Some(true),
..parsed_url ..parsed_url
}), }),
})), })),
@ -115,7 +115,7 @@ impl RequirementsTxtRequirement {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
verbatim: requirement.url.verbatim, verbatim: requirement.url.verbatim,
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
editable: true, editable: Some(true),
..parsed_url ..parsed_url
}), }),
}, },

View file

@ -22,8 +22,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -72,8 +72,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -126,8 +126,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "/scripts/packages/black_editable", install_path: "/scripts/packages/black_editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -176,8 +176,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -226,8 +226,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -276,8 +276,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {

View file

@ -24,8 +24,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -81,8 +83,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -138,8 +142,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -195,8 +201,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -252,8 +260,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -302,8 +312,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable[d", install_path: "<REQUIREMENTS_DIR>/editable[d",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -352,8 +364,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -402,8 +416,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {

View file

@ -22,8 +22,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -72,8 +72,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -126,8 +126,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -176,8 +176,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -226,8 +226,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -276,8 +276,8 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: None,
virtual: false, virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {

View file

@ -24,8 +24,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -81,8 +83,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -138,8 +142,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -195,8 +201,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -252,8 +260,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -302,8 +312,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable[d", install_path: "<REQUIREMENTS_DIR>/editable[d",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -352,8 +364,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
@ -402,8 +416,10 @@ RequirementsTxt {
fragment: None, fragment: None,
}, },
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: Some(
virtual: false, true,
),
virtual: None,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {

View file

@ -154,7 +154,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
let source = SourceUrl::Directory(DirectorySourceUrl { let source = SourceUrl::Directory(DirectorySourceUrl {
url: &url, url: &url,
install_path: Cow::Borrowed(source_tree), 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 // Determine the hash policy. Since we don't have a package name, we perform a

View file

@ -500,7 +500,7 @@ impl<'lock> PylockToml {
.unwrap_or_else(|_| dist.install_path.clone()); .unwrap_or_else(|_| dist.install_path.clone());
package.directory = Some(PylockTomlDirectory { package.directory = Some(PylockTomlDirectory {
path: PortablePathBuf::from(path), path: PortablePathBuf::from(path),
editable: if dist.editable { Some(true) } else { None }, editable: dist.editable,
subdirectory: None, subdirectory: None,
}); });
} }
@ -737,7 +737,7 @@ impl<'lock> PylockToml {
), ),
editable: match editable { editable: match editable {
EditableMode::NonEditable => None, EditableMode::NonEditable => None,
EditableMode::Editable => Some(sdist.editable), EditableMode::Editable => sdist.editable,
}, },
subdirectory: None, subdirectory: None,
}), }),
@ -1394,8 +1394,8 @@ impl PylockTomlDirectory {
Ok(DirectorySourceDist { Ok(DirectorySourceDist {
name: name.clone(), name: name.clone(),
install_path: path.into_boxed_path(), install_path: path.into_boxed_path(),
editable: self.editable.unwrap_or(false), editable: self.editable,
r#virtual: false, r#virtual: Some(false),
url, url,
}) })
} }

View file

@ -2402,8 +2402,8 @@ impl Package {
name: self.id.name.clone(), name: self.id.name.clone(),
url: verbatim_url(&install_path, &self.id)?, url: verbatim_url(&install_path, &self.id)?,
install_path: install_path.into_boxed_path(), install_path: install_path.into_boxed_path(),
editable: false, editable: Some(false),
r#virtual: false, r#virtual: Some(false),
}; };
uv_distribution_types::SourceDist::Directory(dir_dist) uv_distribution_types::SourceDist::Directory(dir_dist)
} }
@ -2413,8 +2413,8 @@ impl Package {
name: self.id.name.clone(), name: self.id.name.clone(),
url: verbatim_url(&install_path, &self.id)?, url: verbatim_url(&install_path, &self.id)?,
install_path: install_path.into_boxed_path(), install_path: install_path.into_boxed_path(),
editable: true, editable: Some(true),
r#virtual: false, r#virtual: Some(false),
}; };
uv_distribution_types::SourceDist::Directory(dir_dist) uv_distribution_types::SourceDist::Directory(dir_dist)
} }
@ -2424,8 +2424,8 @@ impl Package {
name: self.id.name.clone(), name: self.id.name.clone(),
url: verbatim_url(&install_path, &self.id)?, url: verbatim_url(&install_path, &self.id)?,
install_path: install_path.into_boxed_path(), install_path: install_path.into_boxed_path(),
editable: false, editable: Some(false),
r#virtual: true, r#virtual: Some(true),
}; };
uv_distribution_types::SourceDist::Directory(dir_dist) uv_distribution_types::SourceDist::Directory(dir_dist)
} }
@ -3256,9 +3256,9 @@ impl Source {
let path = relative_to(&directory_dist.install_path, root) let path = relative_to(&directory_dist.install_path, root)
.or_else(|_| std::path::absolute(&directory_dist.install_path)) .or_else(|_| std::path::absolute(&directory_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?; .map_err(LockErrorKind::DistributionRelativePath)?;
if directory_dist.editable { if directory_dist.editable.unwrap_or(false) {
Ok(Source::Editable(path.into_boxed_path())) 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())) Ok(Source::Virtual(path.into_boxed_path()))
} else { } else {
Ok(Source::Directory(path.into_boxed_path())) Ok(Source::Directory(path.into_boxed_path()))
@ -4806,8 +4806,8 @@ fn normalize_requirement(
marker: requires_python.simplify_markers(requirement.marker), marker: requires_python.simplify_markers(requirement.marker),
source: RequirementSource::Directory { source: RequirementSource::Directory {
install_path, install_path,
editable, editable: Some(editable.unwrap_or(false)),
r#virtual, r#virtual: Some(r#virtual.unwrap_or(false)),
url, url,
}, },
origin: None, origin: None,

View file

@ -619,6 +619,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&state.python_requirement, &state.python_requirement,
&state.pubgrub, &state.pubgrub,
)?; )?;
match forked_deps { match forked_deps {
ForkedDependencies::Unavailable(reason) => { ForkedDependencies::Unavailable(reason) => {
// Then here, if we get a reason that we consider unrecoverable, we should // Then here, if we get a reason that we consider unrecoverable, we should

View file

@ -63,9 +63,9 @@ impl Urls {
verbatim: _, verbatim: _,
} = package_url } = package_url
{ {
if !*editable { if editable.is_none() {
debug!("Allowing an editable variant of {}", &package_url.verbatim); 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) || is_same_file(&a.install_path, &b.install_path).unwrap_or(false)
} }
(ParsedUrl::Directory(a), ParsedUrl::Directory(b)) => { (ParsedUrl::Directory(a), ParsedUrl::Directory(b)) => {
a.install_path == b.install_path (a.install_path == b.install_path
|| is_same_file(&a.install_path, &b.install_path).unwrap_or(false) || 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, _ => false,
} }

View file

@ -315,15 +315,15 @@ impl Workspace {
source: if member.pyproject_toml.is_package() { source: if member.pyproject_toml.is_package() {
RequirementSource::Directory { RequirementSource::Directory {
install_path: member.root.clone().into_boxed_path(), install_path: member.root.clone().into_boxed_path(),
editable: true, editable: Some(true),
r#virtual: false, r#virtual: Some(false),
url, url,
} }
} else { } else {
RequirementSource::Directory { RequirementSource::Directory {
install_path: member.root.clone().into_boxed_path(), install_path: member.root.clone().into_boxed_path(),
editable: false, editable: Some(false),
r#virtual: true, r#virtual: Some(true),
url, url,
} }
}, },
@ -371,15 +371,15 @@ impl Workspace {
source: if member.pyproject_toml.is_package() { source: if member.pyproject_toml.is_package() {
RequirementSource::Directory { RequirementSource::Directory {
install_path: member.root.clone().into_boxed_path(), install_path: member.root.clone().into_boxed_path(),
editable: true, editable: Some(true),
r#virtual: false, r#virtual: Some(false),
url, url,
} }
} else { } else {
RequirementSource::Directory { RequirementSource::Directory {
install_path: member.root.clone().into_boxed_path(), install_path: member.root.clone().into_boxed_path(),
editable: false, editable: Some(false),
r#virtual: true, r#virtual: Some(true),
url, url,
} }
}, },

View file

@ -802,7 +802,7 @@ fn apply_no_virtual_project(resolution: Resolution) -> Resolution {
return true; 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 { let Dist::Source(SourceDist::Directory(DirectorySourceDist {
name, name,
install_path, install_path,
editable: true, editable: Some(true),
r#virtual: false, r#virtual,
url, url,
})) = dist.as_ref() })) = dist.as_ref()
else { else {
@ -832,8 +832,8 @@ fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolu
dist: Arc::new(Dist::Source(SourceDist::Directory(DirectorySourceDist { dist: Arc::new(Dist::Source(SourceDist::Directory(DirectorySourceDist {
name: name.clone(), name: name.clone(),
install_path: install_path.clone(), install_path: install_path.clone(),
editable: false, editable: Some(false),
r#virtual: false, r#virtual: *r#virtual,
url: url.clone(), url: url.clone(),
}))), }))),
version: version.clone(), version: version.clone(),

View file

@ -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 /// 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] #[test]
fn lock_editable() -> Result<()> { fn lock_editable() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
@ -11084,86 +11084,16 @@ fn lock_editable() -> Result<()> {
library = { path = "../../library", editable = true } library = { path = "../../library", editable = true }
"#})?; "#})?;
uv_snapshot!(context.filters(), context.lock(), @r###" uv_snapshot!(context.filters(), context.lock(), @r"
success: true success: false
exit_code: 0 exit_code: 2
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 3 packages in [TIME] error: Requirements contain conflicting URLs for package `library` in all marker environments:
"###); - file://[TEMP_DIR]/library
- file://[TEMP_DIR]/library (editable)
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)
"###);
Ok(()) Ok(())
} }

View file

@ -9989,3 +9989,343 @@ fn sync_url_with_query_parameters() -> Result<()> {
Ok(()) Ok(())
} }
/// See: <https://github.com/astral-sh/uv/issues/11648>
#[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: <https://github.com/astral-sh/uv/issues/11648>
#[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(())
}