Make Directory its own distribution kind (#3519)

## Summary

I think this is overall good change because it explicitly encodes (in
the type system) something that was previously implicit. I'm not a huge
fan of the names here, open to input.

It covers some of https://github.com/astral-sh/uv/issues/3506 but I
don't think it _closes_ it.
This commit is contained in:
Charlie Marsh 2024-05-13 10:03:14 -04:00 committed by GitHub
parent 6bbfe555be
commit 42c3bfa351
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 434 additions and 163 deletions

View file

@ -6,7 +6,7 @@ use url::Url;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use crate::{GitSourceDist, Name, PathSourceDist, SourceDist}; use crate::{DirectorySourceDist, GitSourceDist, Name, PathSourceDist, SourceDist};
/// A reference to a source that can be built into a built distribution. /// A reference to a source that can be built into a built distribution.
/// ///
@ -62,6 +62,7 @@ pub enum SourceUrl<'a> {
Direct(DirectSourceUrl<'a>), Direct(DirectSourceUrl<'a>),
Git(GitSourceUrl<'a>), Git(GitSourceUrl<'a>),
Path(PathSourceUrl<'a>), Path(PathSourceUrl<'a>),
Directory(DirectorySourceUrl<'a>),
} }
impl<'a> SourceUrl<'a> { impl<'a> SourceUrl<'a> {
@ -71,6 +72,7 @@ impl<'a> SourceUrl<'a> {
Self::Direct(dist) => dist.url, Self::Direct(dist) => dist.url,
Self::Git(dist) => dist.url, Self::Git(dist) => dist.url,
Self::Path(dist) => dist.url, Self::Path(dist) => dist.url,
Self::Directory(dist) => dist.url,
} }
} }
} }
@ -81,6 +83,7 @@ impl std::fmt::Display for SourceUrl<'_> {
Self::Direct(url) => write!(f, "{url}"), Self::Direct(url) => write!(f, "{url}"),
Self::Git(url) => write!(f, "{url}"), Self::Git(url) => write!(f, "{url}"),
Self::Path(url) => write!(f, "{url}"), Self::Path(url) => write!(f, "{url}"),
Self::Directory(url) => write!(f, "{url}"),
} }
} }
} }
@ -133,3 +136,24 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> {
} }
} }
} }
#[derive(Debug, Clone)]
pub struct DirectorySourceUrl<'a> {
pub url: &'a Url,
pub path: Cow<'a, Path>,
}
impl std::fmt::Display for DirectorySourceUrl<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{url}", url = self.url)
}
}
impl<'a> From<&'a DirectorySourceDist> for DirectorySourceUrl<'a> {
fn from(dist: &'a DirectorySourceDist) -> Self {
Self {
url: &dist.url,
path: Cow::Borrowed(&dist.path),
}
}
}

View file

@ -85,6 +85,13 @@ impl CachedDist {
editable: false, editable: false,
}), }),
Dist::Source(SourceDist::Path(dist)) => Self::Url(CachedDirectUrlDist { Dist::Source(SourceDist::Path(dist)) => Self::Url(CachedDirectUrlDist {
filename,
url: dist.url,
hashes,
path,
editable: false,
}),
Dist::Source(SourceDist::Directory(dist)) => Self::Url(CachedDirectUrlDist {
filename, filename,
url: dist.url, url: dist.url,
hashes, hashes,

View file

@ -152,6 +152,7 @@ pub enum SourceDist {
DirectUrl(DirectUrlSourceDist), DirectUrl(DirectUrlSourceDist),
Git(GitSourceDist), Git(GitSourceDist),
Path(PathSourceDist), Path(PathSourceDist),
Directory(DirectorySourceDist),
} }
/// A built distribution (wheel) that exists in a registry, like `PyPI`. /// A built distribution (wheel) that exists in a registry, like `PyPI`.
@ -203,12 +204,20 @@ pub struct GitSourceDist {
pub url: VerbatimUrl, pub url: VerbatimUrl,
} }
/// A source distribution that exists in a local directory. /// A source distribution that exists in a local archive (e.g., a `.tar.gz` file).
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PathSourceDist { pub struct PathSourceDist {
pub name: PackageName, pub name: PackageName,
pub url: VerbatimUrl, pub url: VerbatimUrl,
pub path: PathBuf, pub path: PathBuf,
}
/// A source distribution that exists in a local directory.
#[derive(Debug, Clone)]
pub struct DirectorySourceDist {
pub name: PackageName,
pub url: VerbatimUrl,
pub path: PathBuf,
pub editable: bool, pub editable: bool,
} }
@ -281,7 +290,15 @@ impl Dist {
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
if path // Determine whether the path represents an archive or a directory.
if path.is_dir() {
Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
name,
url,
path,
editable,
})))
} else if path
.extension() .extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl")) .is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
{ {
@ -305,11 +322,14 @@ impl Dist {
path, path,
}))) })))
} else { } else {
if editable {
return Err(Error::EditableFile(url));
}
Ok(Self::Source(SourceDist::Path(PathSourceDist { Ok(Self::Source(SourceDist::Path(PathSourceDist {
name, name,
url, url,
path, path,
editable,
}))) })))
} }
} }
@ -382,7 +402,7 @@ impl Dist {
/// Create a [`Dist`] for a local editable distribution. /// Create a [`Dist`] for a local editable distribution.
pub fn from_editable(name: PackageName, editable: LocalEditable) -> Result<Self, Error> { pub fn from_editable(name: PackageName, editable: LocalEditable) -> Result<Self, Error> {
let LocalEditable { url, path, .. } = editable; let LocalEditable { url, path, .. } = editable;
Ok(Self::Source(SourceDist::Path(PathSourceDist { Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
name, name,
url, url,
path, path,
@ -454,7 +474,7 @@ impl SourceDist {
pub fn index(&self) -> Option<&IndexUrl> { pub fn index(&self) -> Option<&IndexUrl> {
match self { match self {
Self::Registry(registry) => Some(&registry.index), Self::Registry(registry) => Some(&registry.index),
Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) => None, Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None,
} }
} }
@ -462,14 +482,14 @@ impl SourceDist {
pub fn file(&self) -> Option<&File> { pub fn file(&self) -> Option<&File> {
match self { match self {
Self::Registry(registry) => Some(&registry.file), Self::Registry(registry) => Some(&registry.file),
Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) => None, Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None,
} }
} }
pub fn version(&self) -> Option<&Version> { pub fn version(&self) -> Option<&Version> {
match self { match self {
Self::Registry(source_dist) => Some(&source_dist.filename.version), Self::Registry(source_dist) => Some(&source_dist.filename.version),
Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) => None, Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None,
} }
} }
@ -487,7 +507,7 @@ impl SourceDist {
/// Return true if the distribution is editable. /// Return true if the distribution is editable.
pub fn is_editable(&self) -> bool { pub fn is_editable(&self) -> bool {
match self { match self {
Self::Path(PathSourceDist { editable, .. }) => *editable, Self::Directory(DirectorySourceDist { editable, .. }) => *editable,
_ => false, _ => false,
} }
} }
@ -496,6 +516,7 @@ impl SourceDist {
pub fn as_path(&self) -> Option<&Path> { pub fn as_path(&self) -> Option<&Path> {
match self { match self {
Self::Path(dist) => Some(&dist.path), Self::Path(dist) => Some(&dist.path),
Self::Directory(dist) => Some(&dist.path),
_ => None, _ => None,
} }
} }
@ -543,6 +564,12 @@ impl Name for PathSourceDist {
} }
} }
impl Name for DirectorySourceDist {
fn name(&self) -> &PackageName {
&self.name
}
}
impl Name for SourceDist { impl Name for SourceDist {
fn name(&self) -> &PackageName { fn name(&self) -> &PackageName {
match self { match self {
@ -550,6 +577,7 @@ impl Name for SourceDist {
Self::DirectUrl(dist) => dist.name(), Self::DirectUrl(dist) => dist.name(),
Self::Git(dist) => dist.name(), Self::Git(dist) => dist.name(),
Self::Path(dist) => dist.name(), Self::Path(dist) => dist.name(),
Self::Directory(dist) => dist.name(),
} }
} }
} }
@ -615,6 +643,12 @@ impl DistributionMetadata for PathSourceDist {
} }
} }
impl DistributionMetadata for DirectorySourceDist {
fn version_or_url(&self) -> VersionOrUrlRef {
VersionOrUrlRef::Url(&self.url)
}
}
impl DistributionMetadata for SourceDist { impl DistributionMetadata for SourceDist {
fn version_or_url(&self) -> VersionOrUrlRef { fn version_or_url(&self) -> VersionOrUrlRef {
match self { match self {
@ -622,6 +656,7 @@ impl DistributionMetadata for SourceDist {
Self::DirectUrl(dist) => dist.version_or_url(), Self::DirectUrl(dist) => dist.version_or_url(),
Self::Git(dist) => dist.version_or_url(), Self::Git(dist) => dist.version_or_url(),
Self::Path(dist) => dist.version_or_url(), Self::Path(dist) => dist.version_or_url(),
Self::Directory(dist) => dist.version_or_url(),
} }
} }
} }
@ -760,6 +795,16 @@ impl RemoteSource for PathSourceDist {
} }
} }
impl RemoteSource for DirectorySourceDist {
fn filename(&self) -> Result<Cow<'_, str>, Error> {
self.url.filename()
}
fn size(&self) -> Option<u64> {
self.url.size()
}
}
impl RemoteSource for SourceDist { impl RemoteSource for SourceDist {
fn filename(&self) -> Result<Cow<'_, str>, Error> { fn filename(&self) -> Result<Cow<'_, str>, Error> {
match self { match self {
@ -767,6 +812,7 @@ impl RemoteSource for SourceDist {
Self::DirectUrl(dist) => dist.filename(), Self::DirectUrl(dist) => dist.filename(),
Self::Git(dist) => dist.filename(), Self::Git(dist) => dist.filename(),
Self::Path(dist) => dist.filename(), Self::Path(dist) => dist.filename(),
Self::Directory(dist) => dist.filename(),
} }
} }
@ -776,6 +822,7 @@ impl RemoteSource for SourceDist {
Self::DirectUrl(dist) => dist.size(), Self::DirectUrl(dist) => dist.size(),
Self::Git(dist) => dist.size(), Self::Git(dist) => dist.size(),
Self::Path(dist) => dist.size(), Self::Path(dist) => dist.size(),
Self::Directory(dist) => dist.size(),
} }
} }
} }
@ -934,6 +981,16 @@ impl Identifier for PathSourceDist {
} }
} }
impl Identifier for DirectorySourceDist {
fn distribution_id(&self) -> DistributionId {
self.url.distribution_id()
}
fn resource_id(&self) -> ResourceId {
self.url.resource_id()
}
}
impl Identifier for GitSourceDist { impl Identifier for GitSourceDist {
fn distribution_id(&self) -> DistributionId { fn distribution_id(&self) -> DistributionId {
self.url.distribution_id() self.url.distribution_id()
@ -951,6 +1008,7 @@ impl Identifier for SourceDist {
Self::DirectUrl(dist) => dist.distribution_id(), Self::DirectUrl(dist) => dist.distribution_id(),
Self::Git(dist) => dist.distribution_id(), Self::Git(dist) => dist.distribution_id(),
Self::Path(dist) => dist.distribution_id(), Self::Path(dist) => dist.distribution_id(),
Self::Directory(dist) => dist.distribution_id(),
} }
} }
@ -960,6 +1018,7 @@ impl Identifier for SourceDist {
Self::DirectUrl(dist) => dist.resource_id(), Self::DirectUrl(dist) => dist.resource_id(),
Self::Git(dist) => dist.resource_id(), Self::Git(dist) => dist.resource_id(),
Self::Path(dist) => dist.resource_id(), Self::Path(dist) => dist.resource_id(),
Self::Directory(dist) => dist.resource_id(),
} }
} }
} }
@ -1038,12 +1097,23 @@ impl Identifier for PathSourceUrl<'_> {
} }
} }
impl Identifier for DirectorySourceUrl<'_> {
fn distribution_id(&self) -> DistributionId {
self.url.distribution_id()
}
fn resource_id(&self) -> ResourceId {
self.url.resource_id()
}
}
impl Identifier for SourceUrl<'_> { impl Identifier for SourceUrl<'_> {
fn distribution_id(&self) -> DistributionId { fn distribution_id(&self) -> DistributionId {
match self { match self {
Self::Direct(url) => url.distribution_id(), Self::Direct(url) => url.distribution_id(),
Self::Git(url) => url.distribution_id(), Self::Git(url) => url.distribution_id(),
Self::Path(url) => url.distribution_id(), Self::Path(url) => url.distribution_id(),
Self::Directory(url) => url.distribution_id(),
} }
} }
@ -1052,6 +1122,7 @@ impl Identifier for SourceUrl<'_> {
Self::Direct(url) => url.resource_id(), Self::Direct(url) => url.resource_id(),
Self::Git(url) => url.resource_id(), Self::Git(url) => url.resource_id(),
Self::Path(url) => url.resource_id(), Self::Path(url) => url.resource_id(),
Self::Directory(url) => url.resource_id(),
} }
} }
} }

View file

@ -126,6 +126,11 @@ impl From<&ResolvedDist> for Requirement {
url: sdist.url.clone(), url: sdist.url.clone(),
editable: None, editable: None,
}, },
Dist::Source(SourceDist::Directory(sdist)) => RequirementSource::Path {
path: sdist.path.clone(),
url: sdist.url.clone(),
editable: None,
},
}, },
ResolvedDist::Installed(dist) => RequirementSource::Registry { ResolvedDist::Installed(dist) => RequirementSource::Registry {
specifier: pep440_rs::VersionSpecifiers::from( specifier: pep440_rs::VersionSpecifiers::from(

View file

@ -770,41 +770,7 @@ impl ArchiveTimestamp {
if metadata.is_file() { if metadata.is_file() {
Ok(Some(Self::Exact(Timestamp::from_metadata(&metadata)))) Ok(Some(Self::Exact(Timestamp::from_metadata(&metadata))))
} else { } else {
// Compute the modification timestamp for the `pyproject.toml`, `setup.py`, and Self::from_source_tree(path)
// `setup.cfg` files, if they exist.
let pyproject_toml = path
.as_ref()
.join("pyproject.toml")
.metadata()
.ok()
.filter(std::fs::Metadata::is_file)
.as_ref()
.map(Timestamp::from_metadata);
let setup_py = path
.as_ref()
.join("setup.py")
.metadata()
.ok()
.filter(std::fs::Metadata::is_file)
.as_ref()
.map(Timestamp::from_metadata);
let setup_cfg = path
.as_ref()
.join("setup.cfg")
.metadata()
.ok()
.filter(std::fs::Metadata::is_file)
.as_ref()
.map(Timestamp::from_metadata);
// Take the most recent timestamp of the three files.
let Some(timestamp) = max(pyproject_toml, max(setup_py, setup_cfg)) else {
return Ok(None);
};
Ok(Some(Self::Approximate(timestamp)))
} }
} }
@ -814,6 +780,48 @@ impl ArchiveTimestamp {
Ok(Self::Exact(Timestamp::from_metadata(&metadata))) Ok(Self::Exact(Timestamp::from_metadata(&metadata)))
} }
/// Return the modification timestamp for a source tree, i.e., a directory.
///
/// If the source tree doesn't contain an entrypoint (i.e., no `pyproject.toml`, `setup.py`, or
/// `setup.cfg`), returns `None`.
pub fn from_source_tree(path: impl AsRef<Path>) -> Result<Option<Self>, io::Error> {
// Compute the modification timestamp for the `pyproject.toml`, `setup.py`, and
// `setup.cfg` files, if they exist.
let pyproject_toml = path
.as_ref()
.join("pyproject.toml")
.metadata()
.ok()
.filter(std::fs::Metadata::is_file)
.as_ref()
.map(Timestamp::from_metadata);
let setup_py = path
.as_ref()
.join("setup.py")
.metadata()
.ok()
.filter(std::fs::Metadata::is_file)
.as_ref()
.map(Timestamp::from_metadata);
let setup_cfg = path
.as_ref()
.join("setup.cfg")
.metadata()
.ok()
.filter(std::fs::Metadata::is_file)
.as_ref()
.map(Timestamp::from_metadata);
// Take the most recent timestamp of the three files.
let Some(timestamp) = max(pyproject_toml, max(setup_py, setup_cfg)) else {
return Ok(None);
};
Ok(Some(Self::Approximate(timestamp)))
}
/// Return the modification timestamp for an archive. /// Return the modification timestamp for an archive.
pub fn timestamp(&self) -> Timestamp { pub fn timestamp(&self) -> Timestamp {
match self { match self {

View file

@ -1,5 +1,5 @@
use distribution_types::{ use distribution_types::{
git_reference, DirectUrlSourceDist, GitSourceDist, Hashed, PathSourceDist, git_reference, DirectUrlSourceDist, DirectorySourceDist, GitSourceDist, Hashed, PathSourceDist,
}; };
use platform_tags::Tags; use platform_tags::Tags;
use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, CacheShard, WheelCache}; use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, CacheShard, WheelCache};
@ -67,9 +67,43 @@ impl<'a> BuiltWheelIndex<'a> {
return Ok(None); return Ok(None);
}; };
// Determine the last-modified time of the source distribution.
let modified = ArchiveTimestamp::from_file(&source_dist.path).map_err(Error::CacheRead)?;
// If the distribution is stale, omit it from the index.
if !pointer.is_up_to_date(modified) {
return Ok(None);
}
// Enforce hash-checking by omitting any wheels that don't satisfy the required hashes.
let revision = pointer.into_revision();
if !revision.satisfies(self.hasher.get(source_dist)) {
return Ok(None);
}
Ok(self.find(&cache_shard.shard(revision.id())))
}
/// Return the most compatible [`CachedWheel`] for a given source distribution built from a
/// local directory (source tree).
pub fn directory(
&self,
source_dist: &DirectorySourceDist,
) -> Result<Option<CachedWheel>, Error> {
let cache_shard = self.cache.shard(
CacheBucket::BuiltWheels,
WheelCache::Path(&source_dist.url).root(),
);
// Read the revision from the cache.
let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))?
else {
return Ok(None);
};
// Determine the last-modified time of the source distribution. // Determine the last-modified time of the source distribution.
let Some(modified) = let Some(modified) =
ArchiveTimestamp::from_path(&source_dist.path).map_err(Error::CacheRead)? ArchiveTimestamp::from_source_tree(&source_dist.path).map_err(Error::CacheRead)?
else { else {
return Err(Error::DirWithoutEntrypoint); return Err(Error::DirWithoutEntrypoint);
}; };

View file

@ -16,8 +16,9 @@ use zip::ZipArchive;
use distribution_filename::WheelFilename; use distribution_filename::WheelFilename;
use distribution_types::{ use distribution_types::{
BuildableSource, Dist, FileLocation, GitSourceUrl, HashPolicy, Hashed, LocalEditable, BuildableSource, DirectorySourceDist, DirectorySourceUrl, Dist, FileLocation, GitSourceUrl,
ParsedArchiveUrl, PathSourceDist, PathSourceUrl, RemoteSource, SourceDist, SourceUrl, HashPolicy, Hashed, LocalEditable, ParsedArchiveUrl, PathSourceUrl, RemoteSource, SourceDist,
SourceUrl,
}; };
use install_wheel_rs::metadata::read_archive_metadata; use install_wheel_rs::metadata::read_archive_metadata;
use platform_tags::Tags; use platform_tags::Tags;
@ -163,26 +164,25 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
} }
BuildableSource::Dist(SourceDist::Path(dist)) => { BuildableSource::Dist(SourceDist::Directory(dist)) => {
if dist.path.is_dir() { self.source_tree(source, &DirectorySourceUrl::from(dist), tags, hashes)
self.source_tree(source, &PathSourceUrl::from(dist), tags, hashes)
.boxed_local()
.await?
} else {
let cache_shard = self
.build_context
.cache()
.shard(CacheBucket::BuiltWheels, WheelCache::Path(&dist.url).root());
self.archive(
source,
&PathSourceUrl::from(dist),
&cache_shard,
tags,
hashes,
)
.boxed_local() .boxed_local()
.await? .await?
} }
BuildableSource::Dist(SourceDist::Path(dist)) => {
let cache_shard = self
.build_context
.cache()
.shard(CacheBucket::BuiltWheels, WheelCache::Path(&dist.url).root());
self.archive(
source,
&PathSourceUrl::from(dist),
&cache_shard,
tags,
hashes,
)
.boxed_local()
.await?
} }
BuildableSource::Url(SourceUrl::Direct(resource)) => { BuildableSource::Url(SourceUrl::Direct(resource)) => {
let filename = resource let filename = resource
@ -216,20 +216,19 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
} }
BuildableSource::Url(SourceUrl::Directory(resource)) => {
self.source_tree(source, resource, tags, hashes)
.boxed_local()
.await?
}
BuildableSource::Url(SourceUrl::Path(resource)) => { BuildableSource::Url(SourceUrl::Path(resource)) => {
if resource.path.is_dir() { let cache_shard = self.build_context.cache().shard(
self.source_tree(source, resource, tags, hashes) CacheBucket::BuiltWheels,
.boxed_local() WheelCache::Path(resource.url).root(),
.await? );
} else { self.archive(source, resource, &cache_shard, tags, hashes)
let cache_shard = self.build_context.cache().shard( .boxed_local()
CacheBucket::BuiltWheels, .await?
WheelCache::Path(resource.url).root(),
);
self.archive(source, resource, &cache_shard, tags, hashes)
.boxed_local()
.await?
}
} }
}; };
@ -319,20 +318,19 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
} }
BuildableSource::Dist(SourceDist::Directory(dist)) => {
self.source_tree_metadata(source, &DirectorySourceUrl::from(dist), hashes)
.boxed_local()
.await?
}
BuildableSource::Dist(SourceDist::Path(dist)) => { BuildableSource::Dist(SourceDist::Path(dist)) => {
if dist.path.is_dir() { let cache_shard = self
self.source_tree_metadata(source, &PathSourceUrl::from(dist), hashes) .build_context
.boxed_local() .cache()
.await? .shard(CacheBucket::BuiltWheels, WheelCache::Path(&dist.url).root());
} else { self.archive_metadata(source, &PathSourceUrl::from(dist), &cache_shard, hashes)
let cache_shard = self .boxed_local()
.build_context .await?
.cache()
.shard(CacheBucket::BuiltWheels, WheelCache::Path(&dist.url).root());
self.archive_metadata(source, &PathSourceUrl::from(dist), &cache_shard, hashes)
.boxed_local()
.await?
}
} }
BuildableSource::Url(SourceUrl::Direct(resource)) => { BuildableSource::Url(SourceUrl::Direct(resource)) => {
let filename = resource let filename = resource
@ -365,20 +363,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
} }
BuildableSource::Url(SourceUrl::Directory(resource)) => {
self.source_tree_metadata(source, resource, hashes)
.boxed_local()
.await?
}
BuildableSource::Url(SourceUrl::Path(resource)) => { BuildableSource::Url(SourceUrl::Path(resource)) => {
if resource.path.is_dir() { let cache_shard = self.build_context.cache().shard(
self.source_tree_metadata(source, resource, hashes) CacheBucket::BuiltWheels,
.boxed_local() WheelCache::Path(resource.url).root(),
.await? );
} else { self.archive_metadata(source, resource, &cache_shard, hashes)
let cache_shard = self.build_context.cache().shard( .boxed_local()
CacheBucket::BuiltWheels, .await?
WheelCache::Path(resource.url).root(),
);
self.archive_metadata(source, resource, &cache_shard, hashes)
.boxed_local()
.await?
}
} }
}; };
@ -826,7 +824,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
async fn source_tree( async fn source_tree(
&self, &self,
source: &BuildableSource<'_>, source: &BuildableSource<'_>,
resource: &PathSourceUrl<'_>, resource: &DirectorySourceUrl<'_>,
tags: &Tags, tags: &Tags,
hashes: HashPolicy<'_>, hashes: HashPolicy<'_>,
) -> Result<BuiltWheelMetadata, Error> { ) -> Result<BuiltWheelMetadata, Error> {
@ -891,7 +889,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
async fn source_tree_metadata( async fn source_tree_metadata(
&self, &self,
source: &BuildableSource<'_>, source: &BuildableSource<'_>,
resource: &PathSourceUrl<'_>, resource: &DirectorySourceUrl<'_>,
hashes: HashPolicy<'_>, hashes: HashPolicy<'_>,
) -> Result<ArchiveMetadata, Error> { ) -> Result<ArchiveMetadata, Error> {
// Before running the build, check that the hashes match. // Before running the build, check that the hashes match.
@ -967,12 +965,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
async fn source_tree_revision( async fn source_tree_revision(
&self, &self,
source: &BuildableSource<'_>, source: &BuildableSource<'_>,
resource: &PathSourceUrl<'_>, resource: &DirectorySourceUrl<'_>,
cache_shard: &CacheShard, cache_shard: &CacheShard,
) -> Result<Revision, Error> { ) -> Result<Revision, Error> {
// Determine the last-modified time of the source distribution. // Determine the last-modified time of the source distribution.
let Some(modified) = let Some(modified) =
ArchiveTimestamp::from_path(&resource.path).map_err(Error::CacheRead)? ArchiveTimestamp::from_source_tree(&resource.path).map_err(Error::CacheRead)?
else { else {
return Err(Error::DirWithoutEntrypoint); return Err(Error::DirWithoutEntrypoint);
}; };
@ -1432,8 +1430,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.await .await
.map_err(|err| Error::BuildEditable(editable.to_string(), err))?; .map_err(|err| Error::BuildEditable(editable.to_string(), err))?;
let filename = WheelFilename::from_str(&disk_filename)?; let filename = WheelFilename::from_str(&disk_filename)?;
// We finally have the name of the package and can construct the dist. // We finally have the name of the package and can construct the dist.
let dist = Dist::Source(SourceDist::Path(PathSourceDist { let dist = Dist::Source(SourceDist::Directory(DirectorySourceDist {
name: filename.name.clone(), name: filename.name.clone(),
url: editable.url().clone(), url: editable.url().clone(),
path: editable.path.clone(), path: editable.path.clone(),

View file

@ -9,9 +9,10 @@ use tracing::{debug, warn};
use distribution_filename::WheelFilename; use distribution_filename::WheelFilename;
use distribution_types::{ use distribution_types::{
CachedDirectUrlDist, CachedDist, DirectUrlBuiltDist, DirectUrlSourceDist, Error, GitSourceDist, CachedDirectUrlDist, CachedDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
Hashed, IndexLocations, InstalledDist, InstalledMetadata, InstalledVersion, Name, Error, GitSourceDist, Hashed, IndexLocations, InstalledDist, InstalledMetadata,
PathBuiltDist, PathSourceDist, RemoteSource, Requirement, RequirementSource, Verbatim, InstalledVersion, Name, PathBuiltDist, PathSourceDist, RemoteSource, Requirement,
RequirementSource, Verbatim,
}; };
use platform_tags::Tags; use platform_tags::Tags;
use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache}; use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache};
@ -328,7 +329,23 @@ impl<'a> Planner<'a> {
}; };
// Check if we have a wheel or a source distribution. // Check if we have a wheel or a source distribution.
if path if path.is_dir() {
let sdist = DirectorySourceDist {
name: requirement.name.clone(),
url: url.clone(),
path,
editable: false,
};
// 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;
}
} else if path
.extension() .extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl")) .is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
{ {
@ -395,8 +412,8 @@ impl<'a> Planner<'a> {
name: requirement.name.clone(), name: requirement.name.clone(),
url: url.clone(), url: url.clone(),
path, path,
editable: false,
}; };
// Find the most-compatible wheel from the cache, since we don't know // Find the most-compatible wheel from the cache, since we don't know
// the filename in advance. // the filename in advance.
if let Some(wheel) = built_index.path(&sdist)? { if let Some(wheel) = built_index.path(&sdist)? {

View file

@ -7,10 +7,9 @@ use futures::TryStreamExt;
use url::Url; use url::Url;
use distribution_types::{ use distribution_types::{
BuildableSource, HashPolicy, PathSourceUrl, Requirement, SourceUrl, VersionId, BuildableSource, DirectorySourceUrl, HashPolicy, Requirement, SourceUrl, VersionId,
}; };
use pep508_rs::RequirementOrigin; use pep508_rs::RequirementOrigin;
use uv_distribution::{DistributionDatabase, Reporter}; use uv_distribution::{DistributionDatabase, Reporter};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_resolver::{InMemoryIndex, MetadataResponse}; use uv_resolver::{InMemoryIndex, MetadataResponse};
@ -97,7 +96,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
let Ok(url) = Url::from_directory_path(source_tree) else { let Ok(url) = Url::from_directory_path(source_tree) else {
return Err(anyhow::anyhow!("Failed to convert path to URL")); return Err(anyhow::anyhow!("Failed to convert path to URL"));
}; };
let source = SourceUrl::Path(PathSourceUrl { let source = SourceUrl::Directory(DirectorySourceUrl {
url: &url, url: &url,
path: Cow::Borrowed(source_tree), path: Cow::Borrowed(source_tree),
}); });

View file

@ -10,8 +10,9 @@ use tracing::debug;
use distribution_filename::{SourceDistFilename, WheelFilename}; use distribution_filename::{SourceDistFilename, WheelFilename};
use distribution_types::{ use distribution_types::{
BuildableSource, DirectSourceUrl, GitSourceUrl, PathSourceUrl, RemoteSource, Requirement, BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, PathSourceUrl,
SourceUrl, UnresolvedRequirement, UnresolvedRequirementSpecification, VersionId, RemoteSource, Requirement, SourceUrl, UnresolvedRequirement,
UnresolvedRequirementSpecification, VersionId,
}; };
use pep508_rs::{Scheme, UnnamedRequirement, VersionOrUrl}; use pep508_rs::{Scheme, UnnamedRequirement, VersionOrUrl};
use pypi_types::Metadata10; use pypi_types::Metadata10;
@ -222,12 +223,17 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
} }
} }
} }
}
SourceUrl::Path(PathSourceUrl { SourceUrl::Directory(DirectorySourceUrl {
url: &requirement.url, url: &requirement.url,
path: Cow::Owned(path), path: Cow::Owned(path),
}) })
} else {
SourceUrl::Path(PathSourceUrl {
url: &requirement.url,
path: Cow::Owned(path),
})
}
} }
Some(Scheme::Http | Scheme::Https) => SourceUrl::Direct(DirectSourceUrl { Some(Scheme::Http | Scheme::Https) => SourceUrl::Direct(DirectSourceUrl {
url: &requirement.url, url: &requirement.url,

View file

@ -9,10 +9,7 @@ use pubgrub::range::Range;
use pubgrub::report::{DefaultStringReporter, DerivationTree, External, Reporter}; use pubgrub::report::{DefaultStringReporter, DerivationTree, External, Reporter};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use distribution_types::{ use distribution_types::{BuiltDist, IndexLocations, InstalledDist, ParsedUrlError, SourceDist};
BuiltDist, IndexLocations, InstalledDist, ParsedUrlError, PathBuiltDist, PathSourceDist,
SourceDist,
};
use once_map::OnceMap; use once_map::OnceMap;
use pep440_rs::Version; use pep440_rs::Version;
use pep508_rs::Requirement; use pep508_rs::Requirement;
@ -75,14 +72,14 @@ pub enum ResolveError {
FetchAndBuild(Box<SourceDist>, #[source] uv_distribution::Error), FetchAndBuild(Box<SourceDist>, #[source] uv_distribution::Error),
#[error("Failed to read `{0}`")] #[error("Failed to read `{0}`")]
Read(Box<PathBuiltDist>, #[source] uv_distribution::Error), Read(Box<BuiltDist>, #[source] uv_distribution::Error),
// TODO(zanieb): Use `thiserror` in `InstalledDist` so we can avoid chaining `anyhow` // TODO(zanieb): Use `thiserror` in `InstalledDist` so we can avoid chaining `anyhow`
#[error("Failed to read metadata from installed package `{0}`")] #[error("Failed to read metadata from installed package `{0}`")]
ReadInstalled(Box<InstalledDist>, #[source] anyhow::Error), ReadInstalled(Box<InstalledDist>, #[source] anyhow::Error),
#[error("Failed to build `{0}`")] #[error("Failed to build `{0}`")]
Build(Box<PathSourceDist>, #[source] uv_distribution::Error), Build(Box<SourceDist>, #[source] uv_distribution::Error),
#[error(transparent)] #[error(transparent)]
NoSolution(#[from] NoSolutionError), NoSolution(#[from] NoSolutionError),

View file

@ -6,9 +6,10 @@ use std::collections::VecDeque;
use distribution_filename::WheelFilename; use distribution_filename::WheelFilename;
use distribution_types::{ use distribution_types::{
BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, Dist, DistributionMetadata, FileLocation, BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist,
GitSourceDist, IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist, DistributionMetadata, FileLocation, GitSourceDist, IndexUrl, Name, PathBuiltDist,
RegistrySourceDist, Resolution, ResolvedDist, ToUrlError, VersionOrUrlRef, PathSourceDist, RegistryBuiltDist, RegistrySourceDist, Resolution, ResolvedDist, ToUrlError,
VersionOrUrlRef,
}; };
use pep440_rs::Version; use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, VerbatimUrl}; use pep508_rs::{MarkerEnvironment, VerbatimUrl};
@ -360,6 +361,9 @@ impl Source {
distribution_types::SourceDist::Path(ref path_dist) => { distribution_types::SourceDist::Path(ref path_dist) => {
Source::from_path_source_dist(path_dist) Source::from_path_source_dist(path_dist)
} }
distribution_types::SourceDist::Directory(ref directory) => {
Source::from_directory_source_dist(directory)
}
} }
} }
@ -399,6 +403,13 @@ impl Source {
} }
} }
fn from_directory_source_dist(directory_dist: &DirectorySourceDist) -> Source {
Source {
kind: SourceKind::Directory,
url: directory_dist.url.to_url(),
}
}
fn from_index_url(index_url: &IndexUrl) -> Source { fn from_index_url(index_url: &IndexUrl) -> Source {
match *index_url { match *index_url {
IndexUrl::Pypi(ref verbatim_url) => Source { IndexUrl::Pypi(ref verbatim_url) => Source {
@ -497,6 +508,7 @@ pub(crate) enum SourceKind {
Git(GitSource), Git(GitSource),
Direct, Direct,
Path, Path,
Directory,
} }
impl SourceKind { impl SourceKind {
@ -506,6 +518,7 @@ impl SourceKind {
SourceKind::Git(_) => "git", SourceKind::Git(_) => "git",
SourceKind::Direct => "direct", SourceKind::Direct => "direct",
SourceKind::Path => "path", SourceKind::Path => "path",
SourceKind::Directory => "directory",
} }
} }
@ -515,13 +528,8 @@ impl SourceKind {
/// _not_ be present. /// _not_ be present.
fn requires_hash(&self) -> bool { fn requires_hash(&self) -> bool {
match *self { match *self {
SourceKind::Registry | SourceKind::Direct => true, SourceKind::Registry | SourceKind::Direct | SourceKind::Path => true,
// TODO: A `Path` dependency, if it points to a specific source SourceKind::Git(_) | SourceKind::Directory => false,
// distribution or wheel, should have a hash. But if it points to a
// directory, then it should not have a hash.
//
// See: https://github.com/astral-sh/uv/issues/3506
SourceKind::Git(_) | SourceKind::Path => false,
} }
} }
} }
@ -620,6 +628,9 @@ impl SourceDist {
distribution_types::SourceDist::Path(ref path_dist) => { distribution_types::SourceDist::Path(ref path_dist) => {
Ok(SourceDist::from_path_dist(path_dist)) Ok(SourceDist::from_path_dist(path_dist))
} }
distribution_types::SourceDist::Directory(ref directory_dist) => {
Ok(SourceDist::from_directory_dist(directory_dist))
}
} }
} }
@ -659,6 +670,13 @@ impl SourceDist {
hash: None, hash: None,
} }
} }
fn from_directory_dist(directory_dist: &DirectorySourceDist) -> SourceDist {
SourceDist {
url: directory_dist.url.to_url(),
hash: None,
}
}
} }
/// Inspired by: <https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593> /// Inspired by: <https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593>
@ -1148,7 +1166,7 @@ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24b
} }
#[test] #[test]
fn hash_required_missing() { fn hash_optional_missing() {
let data = r#" let data = r#"
version = 1 version = 1

View file

@ -1224,10 +1224,13 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
.boxed_local() .boxed_local()
.await .await
.map_err(|err| match dist.clone() { .map_err(|err| match dist.clone() {
Dist::Built(BuiltDist::Path(built_dist)) => { Dist::Built(built_dist @ BuiltDist::Path(_)) => {
ResolveError::Read(Box::new(built_dist), err) ResolveError::Read(Box::new(built_dist), err)
} }
Dist::Source(SourceDist::Path(source_dist)) => { Dist::Source(source_dist @ SourceDist::Path(_)) => {
ResolveError::Build(Box::new(source_dist), err)
}
Dist::Source(source_dist @ SourceDist::Directory(_)) => {
ResolveError::Build(Box::new(source_dist), err) ResolveError::Build(Box::new(source_dist), err)
} }
Dist::Built(built_dist) => ResolveError::Fetch(Box::new(built_dist), err), Dist::Built(built_dist) => ResolveError::Fetch(Box::new(built_dist), err),
@ -1311,10 +1314,13 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
.boxed_local() .boxed_local()
.await .await
.map_err(|err| match dist.clone() { .map_err(|err| match dist.clone() {
Dist::Built(BuiltDist::Path(built_dist)) => { Dist::Built(built_dist @ BuiltDist::Path(_)) => {
ResolveError::Read(Box::new(built_dist), err) ResolveError::Read(Box::new(built_dist), err)
} }
Dist::Source(SourceDist::Path(source_dist)) => { Dist::Source(source_dist @ SourceDist::Path(_)) => {
ResolveError::Build(Box::new(source_dist), err)
}
Dist::Source(source_dist @ SourceDist::Directory(_)) => {
ResolveError::Build(Box::new(source_dist), err) ResolveError::Build(Box::new(source_dist), err)
} }
Dist::Built(built_dist) => { Dist::Built(built_dist) => {

View file

@ -0,0 +1,96 @@
---
source: crates/uv-resolver/src/lock.rs
expression: result
---
Ok(
Lock {
version: 1,
distributions: [
Distribution {
id: DistributionId {
name: PackageName(
"anyio",
),
version: "4.3.0",
source: Source {
kind: Path,
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/foo/bar",
query: None,
fragment: None,
},
},
},
marker: None,
sourcedist: None,
wheels: [
Wheel {
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/foo/bar/anyio-4.3.0-py3-none-any.whl",
query: None,
fragment: None,
},
hash: Some(
Hash(
HashDigest {
algorithm: Sha256,
digest: "048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8",
},
),
),
filename: WheelFilename {
name: PackageName(
"anyio",
),
version: "4.3.0",
python_tag: [
"py3",
],
abi_tag: [
"none",
],
platform_tag: [
"any",
],
},
},
],
dependencies: [],
},
],
by_id: {
DistributionId {
name: PackageName(
"anyio",
),
version: "4.3.0",
source: Source {
kind: Path,
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/foo/bar",
query: None,
fragment: None,
},
},
}: 0,
},
},
)

View file

@ -1,16 +0,0 @@
---
source: crates/uv-resolver/src/lock.rs
expression: result
---
Err(
Error {
inner: Error {
inner: TomlError {
message: "since the distribution `anyio 4.3.0 path+file:///foo/bar` comes from a path dependency, a hash was not expected but one was found for wheel",
raw: None,
keys: [],
span: None,
},
},
},
)

View file

@ -2609,7 +2609,7 @@ requires-python = ">=3.8"
"### "###
); );
// Modify the editable package. // Modify the package.
pyproject_toml.write_str( pyproject_toml.write_str(
r#"[project] r#"[project]
name = "example" name = "example"