Use separate path types for directories and files (#4285)

## Summary

This is what I consider to be the "real" fix for #8072. We now treat
directory and path URLs as separate `ParsedUrl` types and
`RequirementSource` types. This removes a lot of `.is_dir()` forking
within the `ParsedUrl::Path` arms and makes some states impossible
(e.g., you can't have a `.whl` path that is editable). It _also_ fixes
the `direct_url.json` for direct URLs that refer to files. Previously,
we wrote out to these as if they were installed as directories, which is
just wrong.
This commit is contained in:
Charlie Marsh 2024-06-12 12:59:21 -07:00 committed by GitHub
parent c4483017ac
commit d8f1de6134
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 524 additions and 167 deletions

View file

@ -272,7 +272,8 @@ impl<'a> Planner<'a> {
continue;
}
}
RequirementSource::Path {
RequirementSource::Directory {
url,
editable,
install_path,
@ -287,25 +288,40 @@ impl<'a> Planner<'a> {
Err(err) => return Err(err.into()),
};
// Check if we have a wheel or a source distribution.
if path.is_dir() {
let sdist = DirectorySourceDist {
name: requirement.name.clone(),
url: url.clone(),
install_path: path,
lock_path: lock_path.clone(),
editable: *editable,
};
let sdist = DirectorySourceDist {
name: requirement.name.clone(),
url: url.clone(),
install_path: path,
lock_path: lock_path.clone(),
editable: *editable,
};
// Find the most-compatible wheel from the cache, since we don't know
// the filename in advance.
if let Some(wheel) = built_index.directory(&sdist)? {
let cached_dist = wheel.into_url_dist(url.clone());
debug!("Path source requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
// Find the most-compatible wheel from the cache, since we don't know
// the filename in advance.
if let Some(wheel) = built_index.directory(&sdist)? {
let cached_dist = wheel.into_url_dist(url.clone());
debug!("Directory source requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
}
RequirementSource::Path {
url,
install_path,
lock_path,
} => {
// Store the canonicalized path, which also serves to validate that it exists.
let path = match install_path.canonicalize() {
Ok(path) => path,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::NotFound(url.to_url()).into());
}
} else if path
Err(err) => return Err(err.into()),
};
// Check if we have a wheel or a source distribution.
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
{

View file

@ -149,6 +149,52 @@ impl RequirementSatisfaction {
Ok(Self::Satisfied)
}
RequirementSource::Path {
install_path: requested_path,
lock_path: _,
url: _,
} => {
let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution
else {
return Ok(Self::Mismatch);
};
let DirectUrl::ArchiveUrl {
url: installed_url,
archive_info: _,
subdirectory: None,
} = direct_url.as_ref()
else {
return Ok(Self::Mismatch);
};
let Some(installed_path) = Url::parse(installed_url)
.ok()
.and_then(|url| url.to_file_path().ok())
else {
return Ok(Self::Mismatch);
};
if !(*requested_path == installed_path
|| is_same_file(requested_path, &installed_path).unwrap_or(false))
{
trace!(
"Path mismatch: {:?} vs. {:?}",
requested_path,
installed_path
);
return Ok(Self::Satisfied);
}
if !ArchiveTimestamp::up_to_date_with(
requested_path,
ArchiveTarget::Install(distribution),
)? {
trace!("Installed package is out of date");
return Ok(Self::OutOfDate);
}
Ok(Self::Satisfied)
}
RequirementSource::Directory {
install_path: requested_path,
lock_path: _,
editable: requested_editable,
@ -205,13 +251,11 @@ impl RequirementSatisfaction {
}
// Does the package have dynamic metadata?
// TODO(charlie): Separate `RequirementSource` into `Path` and `Directory`.
if requested_path.is_dir() && is_dynamic(requested_path) {
if is_dynamic(requested_path) {
trace!("Dependency is dynamic");
return Ok(Self::Dynamic);
}
// Otherwise, assume the requirement is up-to-date.
Ok(Self::Satisfied)
}
}