mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-30 07:17:26 +00:00
Omit dynamic versions from the lockfile (#10622)
## Summary This PR modifies the lockfile to omit versions for source trees that use `dynamic` versioning, thereby enabling projects to use dynamic versioning with `uv.lock`. Prior to this change, dynamic versioning was largely incompatible with locking, especially for popular tools like `setuptools_scm` -- in that case, every commit bumps the version, so every commit invalidates the committed lockfile. Closes https://github.com/astral-sh/uv/issues/7533.
This commit is contained in:
parent
d20a48a5b4
commit
0617fd5da6
28 changed files with 990 additions and 366 deletions
|
@ -51,6 +51,7 @@ impl DependencyMetadata {
|
|||
requires_dist: metadata.requires_dist.clone(),
|
||||
requires_python: metadata.requires_python.clone(),
|
||||
provides_extras: metadata.provides_extras.clone(),
|
||||
dynamic: false,
|
||||
})
|
||||
} else {
|
||||
// If no version was requested (i.e., it's a direct URL dependency), allow a single
|
||||
|
@ -70,6 +71,7 @@ impl DependencyMetadata {
|
|||
requires_dist: metadata.requires_dist.clone(),
|
||||
requires_python: metadata.requires_python.clone(),
|
||||
provides_extras: metadata.provides_extras.clone(),
|
||||
dynamic: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ pub struct Metadata {
|
|||
pub requires_python: Option<VersionSpecifiers>,
|
||||
pub provides_extras: Vec<ExtraName>,
|
||||
pub dependency_groups: BTreeMap<GroupName, Vec<uv_pypi_types::Requirement>>,
|
||||
pub dynamic: bool,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
|
@ -67,6 +68,7 @@ impl Metadata {
|
|||
requires_python: metadata.requires_python,
|
||||
provides_extras: metadata.provides_extras,
|
||||
dependency_groups: BTreeMap::default(),
|
||||
dynamic: metadata.dynamic,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,6 +111,7 @@ impl Metadata {
|
|||
requires_python: metadata.requires_python,
|
||||
provides_extras,
|
||||
dependency_groups,
|
||||
dynamic: metadata.dynamic,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -535,14 +535,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
}
|
||||
|
||||
// If the metadata is static, return it.
|
||||
if let Some(metadata) =
|
||||
Self::read_static_metadata(source, source_dist_entry.path(), subdirectory).await?
|
||||
{
|
||||
return Ok(ArchiveMetadata {
|
||||
metadata: Metadata::from_metadata23(metadata),
|
||||
hashes: revision.into_hashes(),
|
||||
});
|
||||
}
|
||||
let dynamic =
|
||||
match StaticMetadata::read(source, source_dist_entry.path(), subdirectory).await? {
|
||||
StaticMetadata::Some(metadata) => {
|
||||
return Ok(ArchiveMetadata {
|
||||
metadata: Metadata::from_metadata23(metadata),
|
||||
hashes: revision.into_hashes(),
|
||||
});
|
||||
}
|
||||
StaticMetadata::Dynamic => true,
|
||||
StaticMetadata::None => false,
|
||||
};
|
||||
|
||||
// If the cache contains compatible metadata, return it.
|
||||
let metadata_entry = cache_shard.entry(METADATA);
|
||||
|
@ -593,6 +596,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
.boxed_local()
|
||||
.await?
|
||||
{
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
fs::create_dir_all(metadata_entry.dir())
|
||||
.await
|
||||
|
@ -623,17 +636,27 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
)
|
||||
.await?;
|
||||
|
||||
// Store the metadata.
|
||||
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
|
||||
.await
|
||||
.map_err(Error::CacheWrite)?;
|
||||
|
||||
if let Some(task) = task {
|
||||
if let Some(reporter) = self.reporter.as_ref() {
|
||||
reporter.on_build_complete(source, task);
|
||||
}
|
||||
}
|
||||
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
|
||||
.await
|
||||
.map_err(Error::CacheWrite)?;
|
||||
|
||||
Ok(ArchiveMetadata {
|
||||
metadata: Metadata::from_metadata23(metadata),
|
||||
hashes: revision.into_hashes(),
|
||||
|
@ -844,14 +867,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
let source_entry = cache_shard.entry(SOURCE);
|
||||
|
||||
// If the metadata is static, return it.
|
||||
if let Some(metadata) =
|
||||
Self::read_static_metadata(source, source_entry.path(), None).await?
|
||||
{
|
||||
return Ok(ArchiveMetadata {
|
||||
metadata: Metadata::from_metadata23(metadata),
|
||||
hashes: revision.into_hashes(),
|
||||
});
|
||||
}
|
||||
let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
|
||||
StaticMetadata::Some(metadata) => {
|
||||
return Ok(ArchiveMetadata {
|
||||
metadata: Metadata::from_metadata23(metadata),
|
||||
hashes: revision.into_hashes(),
|
||||
});
|
||||
}
|
||||
StaticMetadata::Dynamic => true,
|
||||
StaticMetadata::None => false,
|
||||
};
|
||||
|
||||
// If the cache contains compatible metadata, return it.
|
||||
let metadata_entry = cache_shard.entry(METADATA);
|
||||
|
@ -880,6 +905,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
.boxed_local()
|
||||
.await?
|
||||
{
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
fs::create_dir_all(metadata_entry.dir())
|
||||
.await
|
||||
|
@ -924,6 +959,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
|
||||
.await
|
||||
|
@ -1093,21 +1138,24 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
|
||||
}
|
||||
|
||||
if let Some(metadata) =
|
||||
Self::read_static_metadata(source, &resource.install_path, None).await?
|
||||
{
|
||||
return Ok(ArchiveMetadata::from(
|
||||
Metadata::from_workspace(
|
||||
metadata,
|
||||
resource.install_path.as_ref(),
|
||||
None,
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources(),
|
||||
self.build_context.bounds(),
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
// If the metadata is static, return it.
|
||||
let dynamic = match StaticMetadata::read(source, &resource.install_path, None).await? {
|
||||
StaticMetadata::Some(metadata) => {
|
||||
return Ok(ArchiveMetadata::from(
|
||||
Metadata::from_workspace(
|
||||
metadata,
|
||||
resource.install_path.as_ref(),
|
||||
None,
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources(),
|
||||
self.build_context.bounds(),
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
StaticMetadata::Dynamic => true,
|
||||
StaticMetadata::None => false,
|
||||
};
|
||||
|
||||
let cache_shard = self.build_context.cache().shard(
|
||||
CacheBucket::SourceDistributions,
|
||||
|
@ -1160,6 +1208,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
.boxed_local()
|
||||
.await?
|
||||
{
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
fs::create_dir_all(metadata_entry.dir())
|
||||
.await
|
||||
|
@ -1211,6 +1269,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
|
||||
.await
|
||||
|
@ -1472,21 +1540,25 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
git_source: resource,
|
||||
};
|
||||
|
||||
if let Some(metadata) =
|
||||
Self::read_static_metadata(source, fetch.path(), resource.subdirectory).await?
|
||||
{
|
||||
return Ok(ArchiveMetadata::from(
|
||||
Metadata::from_workspace(
|
||||
metadata,
|
||||
&path,
|
||||
Some(&git_member),
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources(),
|
||||
self.build_context.bounds(),
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
// If the metadata is static, return it.
|
||||
let dynamic =
|
||||
match StaticMetadata::read(source, fetch.path(), resource.subdirectory).await? {
|
||||
StaticMetadata::Some(metadata) => {
|
||||
return Ok(ArchiveMetadata::from(
|
||||
Metadata::from_workspace(
|
||||
metadata,
|
||||
&path,
|
||||
Some(&git_member),
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources(),
|
||||
self.build_context.bounds(),
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
StaticMetadata::Dynamic => true,
|
||||
StaticMetadata::None => false,
|
||||
};
|
||||
|
||||
// If the cache contains compatible metadata, return it.
|
||||
if self
|
||||
|
@ -1531,6 +1603,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
.boxed_local()
|
||||
.await?
|
||||
{
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
fs::create_dir_all(metadata_entry.dir())
|
||||
.await
|
||||
|
@ -1582,6 +1664,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
// If necessary, mark the metadata as dynamic.
|
||||
let metadata = if dynamic {
|
||||
ResolutionMetadata {
|
||||
dynamic: true,
|
||||
..metadata
|
||||
}
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
|
||||
// Store the metadata.
|
||||
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
|
||||
.await
|
||||
|
@ -2025,122 +2117,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
Ok(Some(metadata))
|
||||
}
|
||||
|
||||
async fn read_static_metadata(
|
||||
source: &BuildableSource<'_>,
|
||||
source_root: &Path,
|
||||
subdirectory: Option<&Path>,
|
||||
) -> Result<Option<ResolutionMetadata>, Error> {
|
||||
// Attempt to read static metadata from the `pyproject.toml`.
|
||||
match read_pyproject_toml(source_root, subdirectory, source.version()).await {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `pyproject.toml` for: {source}");
|
||||
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `pyproject.toml` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingPyprojectToml
|
||||
| Error::PyprojectToml(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::DynamicField(_)
|
||||
| uv_pypi_types::MetadataError::FieldNotFound(_)
|
||||
| uv_pypi_types::MetadataError::PoetrySyntax,
|
||||
)),
|
||||
) => {
|
||||
debug!("No static `pyproject.toml` available for: {source} ({err:?})");
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
// If the source distribution is a source tree, avoid reading `PKG-INFO` or `egg-info`,
|
||||
// since they could be out-of-date.
|
||||
if source.is_source_tree() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Attempt to read static metadata from the `PKG-INFO` file.
|
||||
match read_pkg_info(source_root, subdirectory).await {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `PKG-INFO` for: {source}");
|
||||
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `PKG-INFO` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingPkgInfo
|
||||
| Error::PkgInfo(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::DynamicField(_)
|
||||
| uv_pypi_types::MetadataError::FieldNotFound(_)
|
||||
| uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_),
|
||||
)),
|
||||
) => {
|
||||
debug!("No static `PKG-INFO` available for: {source} ({err:?})");
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
// Attempt to read static metadata from the `egg-info` directory.
|
||||
match read_egg_info(source_root, subdirectory, source.name(), source.version()).await {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `egg-info` for: {source}");
|
||||
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `egg-info` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingEggInfo
|
||||
| Error::MissingRequiresTxt
|
||||
| Error::MissingPkgInfo
|
||||
| Error::RequiresTxt(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::RequiresTxtContents(_),
|
||||
)
|
||||
| Error::PkgInfo(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::DynamicField(_)
|
||||
| uv_pypi_types::MetadataError::FieldNotFound(_)
|
||||
| uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_),
|
||||
)),
|
||||
) => {
|
||||
debug!("No static `egg-info` available for: {source} ({err:?})");
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns a GET [`reqwest::Request`] for the given URL.
|
||||
fn request(url: Url, client: &RegistryClient) -> Result<reqwest::Request, reqwest::Error> {
|
||||
client
|
||||
|
@ -2225,6 +2201,146 @@ pub fn prune(cache: &Cache) -> Result<Removal, Error> {
|
|||
Ok(removal)
|
||||
}
|
||||
|
||||
/// The result of extracting statically available metadata from a source distribution.
|
||||
#[derive(Debug)]
|
||||
enum StaticMetadata {
|
||||
/// The metadata was found and successfully read.
|
||||
Some(ResolutionMetadata),
|
||||
/// The metadata was found, but it was ignored due to a dynamic version.
|
||||
Dynamic,
|
||||
/// The metadata was not found.
|
||||
None,
|
||||
}
|
||||
|
||||
impl StaticMetadata {
|
||||
/// Read the [`ResolutionMetadata`] from a source distribution.
|
||||
async fn read(
|
||||
source: &BuildableSource<'_>,
|
||||
source_root: &Path,
|
||||
subdirectory: Option<&Path>,
|
||||
) -> Result<Self, Error> {
|
||||
// Attempt to read static metadata from the `pyproject.toml`.
|
||||
match read_pyproject_toml(source_root, subdirectory, source.version()).await {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `pyproject.toml` for: {source}");
|
||||
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Self::Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `pyproject.toml` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ Error::PyprojectToml(uv_pypi_types::MetadataError::DynamicField("version")),
|
||||
) if source.is_source_tree() => {
|
||||
// In Metadata 2.2, `Dynamic` was introduced to Core Metadata to indicate that a
|
||||
// given field was marked as dynamic in the originating source tree. However, we may
|
||||
// be looking at a distribution with a build backend that doesn't support Metadata 2.2. In that case,
|
||||
// we want to infer the `Dynamic` status from the `pyproject.toml` file, if available.
|
||||
debug!("No static `pyproject.toml` available for: {source} ({err:?})");
|
||||
return Ok(Self::Dynamic);
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingPyprojectToml
|
||||
| Error::PyprojectToml(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::DynamicField(_)
|
||||
| uv_pypi_types::MetadataError::FieldNotFound(_)
|
||||
| uv_pypi_types::MetadataError::PoetrySyntax,
|
||||
)),
|
||||
) => {
|
||||
debug!("No static `pyproject.toml` available for: {source} ({err:?})");
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
// If the source distribution is a source tree, avoid reading `PKG-INFO` or `egg-info`,
|
||||
// since they could be out-of-date.
|
||||
if source.is_source_tree() {
|
||||
return Ok(Self::None);
|
||||
}
|
||||
|
||||
// Attempt to read static metadata from the `PKG-INFO` file.
|
||||
match read_pkg_info(source_root, subdirectory).await {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `PKG-INFO` for: {source}");
|
||||
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Self::Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `PKG-INFO` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingPkgInfo
|
||||
| Error::PkgInfo(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::DynamicField(_)
|
||||
| uv_pypi_types::MetadataError::FieldNotFound(_)
|
||||
| uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_),
|
||||
)),
|
||||
) => {
|
||||
debug!("No static `PKG-INFO` available for: {source} ({err:?})");
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
// Attempt to read static metadata from the `egg-info` directory.
|
||||
match read_egg_info(source_root, subdirectory, source.name(), source.version()).await {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `egg-info` for: {source}");
|
||||
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Self::Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `egg-info` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingEggInfo
|
||||
| Error::MissingRequiresTxt
|
||||
| Error::MissingPkgInfo
|
||||
| Error::RequiresTxt(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::RequiresTxtContents(_),
|
||||
)
|
||||
| Error::PkgInfo(
|
||||
uv_pypi_types::MetadataError::Pep508Error(_)
|
||||
| uv_pypi_types::MetadataError::DynamicField(_)
|
||||
| uv_pypi_types::MetadataError::FieldNotFound(_)
|
||||
| uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_),
|
||||
)),
|
||||
) => {
|
||||
debug!("No static `egg-info` available for: {source} ({err:?})");
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
Ok(Self::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that the source distribution matches the built metadata.
|
||||
fn validate_metadata(
|
||||
source: &BuildableSource<'_>,
|
||||
|
@ -2464,6 +2580,9 @@ async fn read_egg_info(
|
|||
// Parse the metadata.
|
||||
let metadata = Metadata12::parse_metadata(&content).map_err(Error::PkgInfo)?;
|
||||
|
||||
// Determine whether the version is dynamic.
|
||||
let dynamic = metadata.dynamic.iter().any(|field| field == "version");
|
||||
|
||||
// Combine the sources.
|
||||
Ok(ResolutionMetadata {
|
||||
name: metadata.name,
|
||||
|
@ -2471,6 +2590,7 @@ async fn read_egg_info(
|
|||
requires_python: metadata.requires_python,
|
||||
requires_dist: requires_txt.requires_dist,
|
||||
provides_extras: requires_txt.provides_extras,
|
||||
dynamic,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ use uv_normalize::PackageName;
|
|||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
|
||||
/// A subset of the full cure metadata specification, only including the
|
||||
/// fields that have been consistent across all versions of the specification later than 1.2.
|
||||
/// fields that have been consistent across all versions of the specification later than 1.2, with
|
||||
/// the exception of `Dynamic`, which is optional (but introduced in Metadata 2.2).
|
||||
///
|
||||
/// Python Package Metadata 1.2 is specified in <https://peps.python.org/pep-0345/>.
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
|
@ -15,6 +16,7 @@ pub struct Metadata12 {
|
|||
pub name: PackageName,
|
||||
pub version: Version,
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
pub dynamic: Vec<String>,
|
||||
}
|
||||
|
||||
impl Metadata12 {
|
||||
|
@ -54,11 +56,13 @@ impl Metadata12 {
|
|||
.map(|requires_python| LenientVersionSpecifiers::from_str(&requires_python))
|
||||
.transpose()?
|
||||
.map(VersionSpecifiers::from);
|
||||
let dynamic = headers.get_all_values("Dynamic").collect::<Vec<_>>();
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
version,
|
||||
requires_python,
|
||||
dynamic,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ pub struct ResolutionMetadata {
|
|||
pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>,
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
pub provides_extras: Vec<ExtraName>,
|
||||
/// Whether the version field is dynamic.
|
||||
#[serde(default)]
|
||||
pub dynamic: bool,
|
||||
}
|
||||
|
||||
/// From <https://github.com/PyO3/python-pkginfo-rs/blob/d719988323a0cfea86d4737116d7917f30e819e2/src/metadata.rs#LL78C2-L91C26>
|
||||
|
@ -68,6 +71,9 @@ impl ResolutionMetadata {
|
|||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let dynamic = headers
|
||||
.get_all_values("Dynamic")
|
||||
.any(|field| field == "Version");
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
|
@ -75,6 +81,7 @@ impl ResolutionMetadata {
|
|||
requires_dist,
|
||||
requires_python,
|
||||
provides_extras,
|
||||
dynamic,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -97,12 +104,13 @@ impl ResolutionMetadata {
|
|||
}
|
||||
|
||||
// If any of the fields we need are marked as dynamic, we can't use the `PKG-INFO` file.
|
||||
let dynamic = headers.get_all_values("Dynamic").collect::<Vec<_>>();
|
||||
for field in dynamic {
|
||||
let mut dynamic = false;
|
||||
for field in headers.get_all_values("Dynamic") {
|
||||
match field.as_str() {
|
||||
"Requires-Python" => return Err(MetadataError::DynamicField("Requires-Python")),
|
||||
"Requires-Dist" => return Err(MetadataError::DynamicField("Requires-Dist")),
|
||||
"Provides-Extra" => return Err(MetadataError::DynamicField("Provides-Extra")),
|
||||
"Version" => dynamic = true,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +156,7 @@ impl ResolutionMetadata {
|
|||
requires_dist,
|
||||
requires_python,
|
||||
provides_extras,
|
||||
dynamic,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -29,8 +29,8 @@ pub(crate) fn parse_pyproject_toml(
|
|||
.ok_or(MetadataError::FieldNotFound("project"))?;
|
||||
|
||||
// If any of the fields we need were declared as dynamic, we can't use the `pyproject.toml` file.
|
||||
let dynamic = project.dynamic.unwrap_or_default();
|
||||
for field in dynamic {
|
||||
let mut dynamic = false;
|
||||
for field in project.dynamic.unwrap_or_default() {
|
||||
match field.as_str() {
|
||||
"dependencies" => return Err(MetadataError::DynamicField("dependencies")),
|
||||
"optional-dependencies" => {
|
||||
|
@ -39,8 +39,11 @@ pub(crate) fn parse_pyproject_toml(
|
|||
"requires-python" => return Err(MetadataError::DynamicField("requires-python")),
|
||||
// When building from a source distribution, the version is known from the filename and
|
||||
// fixed by it, so we can pretend it's static.
|
||||
"version" if sdist_version.is_none() => {
|
||||
return Err(MetadataError::DynamicField("version"))
|
||||
"version" => {
|
||||
if sdist_version.is_none() {
|
||||
return Err(MetadataError::DynamicField("version"));
|
||||
}
|
||||
dynamic = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -99,6 +102,7 @@ pub(crate) fn parse_pyproject_toml(
|
|||
requires_dist,
|
||||
requires_python,
|
||||
provides_extras,
|
||||
dynamic,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,9 @@ pub fn read_lock_requirements(
|
|||
}
|
||||
|
||||
// Map each entry in the lockfile to a preference.
|
||||
preferences.push(Preference::from_lock(package, install_path)?);
|
||||
if let Some(preference) = Preference::from_lock(package, install_path)? {
|
||||
preferences.push(preference);
|
||||
}
|
||||
|
||||
// Map each entry in the lockfile to a Git SHA.
|
||||
if let Some(git_ref) = package.as_git_ref()? {
|
||||
|
|
|
@ -344,11 +344,8 @@ pub trait Installable<'lock> {
|
|||
TagPolicy::Required(tags),
|
||||
build_options,
|
||||
)?;
|
||||
let version = package.version().clone();
|
||||
let dist = ResolvedDist::Installable {
|
||||
dist,
|
||||
version: Some(version),
|
||||
};
|
||||
let version = package.version().cloned();
|
||||
let dist = ResolvedDist::Installable { dist, version };
|
||||
let hashes = package.hashes();
|
||||
Ok(Node::Dist {
|
||||
dist,
|
||||
|
@ -364,11 +361,8 @@ pub trait Installable<'lock> {
|
|||
TagPolicy::Preferred(tags),
|
||||
&BuildOptions::default(),
|
||||
)?;
|
||||
let version = package.version().clone();
|
||||
let dist = ResolvedDist::Installable {
|
||||
dist,
|
||||
version: Some(version),
|
||||
};
|
||||
let version = package.version().cloned();
|
||||
let dist = ResolvedDist::Installable { dist, version };
|
||||
let hashes = package.hashes();
|
||||
Ok(Node::Dist {
|
||||
dist,
|
||||
|
|
|
@ -1004,31 +1004,20 @@ impl Lock {
|
|||
.flatten()
|
||||
.map(|package| matches!(package.id.source, Source::Virtual(_)));
|
||||
if actual != Some(expected) {
|
||||
return Ok(SatisfiesResult::MismatchedSources(name.clone(), expected));
|
||||
return Ok(SatisfiesResult::MismatchedVirtual(name.clone(), expected));
|
||||
}
|
||||
}
|
||||
|
||||
// E.g., that the version has changed.
|
||||
// E.g., that they've switched from dynamic to non-dynamic or vice versa.
|
||||
for (name, member) in packages {
|
||||
let Some(expected) = member
|
||||
.pyproject_toml()
|
||||
.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.version.as_ref())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let expected = member.pyproject_toml().is_dynamic();
|
||||
let actual = self
|
||||
.find_by_name(name)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|package| &package.id.version);
|
||||
.map(Package::is_dynamic);
|
||||
if actual != Some(expected) {
|
||||
return Ok(SatisfiesResult::MismatchedVersion(
|
||||
name.clone(),
|
||||
expected.clone(),
|
||||
actual.cloned(),
|
||||
));
|
||||
return Ok(SatisfiesResult::MismatchedDynamic(name.clone(), expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1196,20 +1185,24 @@ impl Lock {
|
|||
.as_ref()
|
||||
.is_some_and(|remotes| !remotes.contains(url))
|
||||
{
|
||||
return Ok(SatisfiesResult::MissingRemoteIndex(
|
||||
&package.id.name,
|
||||
&package.id.version,
|
||||
url,
|
||||
));
|
||||
let name = &package.id.name;
|
||||
let version = &package
|
||||
.id
|
||||
.version
|
||||
.as_ref()
|
||||
.expect("version for registry source");
|
||||
return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
|
||||
}
|
||||
}
|
||||
RegistrySource::Path(path) => {
|
||||
if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
|
||||
return Ok(SatisfiesResult::MissingLocalIndex(
|
||||
&package.id.name,
|
||||
&package.id.version,
|
||||
path,
|
||||
));
|
||||
let name = &package.id.name;
|
||||
let version = &package
|
||||
.id
|
||||
.version
|
||||
.as_ref()
|
||||
.expect("version for registry source");
|
||||
return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1233,6 +1226,9 @@ impl Lock {
|
|||
)?;
|
||||
|
||||
// Fetch the metadata for the distribution.
|
||||
//
|
||||
// TODO(charlie): We don't need the version here, so we could avoid running a PEP 517
|
||||
// build if only the version is dynamic.
|
||||
let metadata = {
|
||||
let id = dist.version_id();
|
||||
if let Some(archive) =
|
||||
|
@ -1271,15 +1267,6 @@ impl Lock {
|
|||
}
|
||||
};
|
||||
|
||||
// Validate the `version` metadata.
|
||||
if metadata.version != package.id.version {
|
||||
return Ok(SatisfiesResult::MismatchedVersion(
|
||||
package.id.name.clone(),
|
||||
package.id.version.clone(),
|
||||
Some(metadata.version.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate the `requires-dist` metadata.
|
||||
{
|
||||
let expected: BTreeSet<_> = metadata
|
||||
|
@ -1298,7 +1285,7 @@ impl Lock {
|
|||
if expected != actual {
|
||||
return Ok(SatisfiesResult::MismatchedPackageRequirements(
|
||||
&package.id.name,
|
||||
&package.id.version,
|
||||
package.id.version.as_ref(),
|
||||
expected,
|
||||
actual,
|
||||
));
|
||||
|
@ -1341,7 +1328,7 @@ impl Lock {
|
|||
if expected != actual {
|
||||
return Ok(SatisfiesResult::MismatchedPackageDependencyGroups(
|
||||
&package.id.name,
|
||||
&package.id.version,
|
||||
package.id.version.as_ref(),
|
||||
expected,
|
||||
actual,
|
||||
));
|
||||
|
@ -1406,8 +1393,10 @@ pub enum SatisfiesResult<'lock> {
|
|||
Satisfied,
|
||||
/// The lockfile uses a different set of workspace members.
|
||||
MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
|
||||
/// The lockfile uses a different set of sources for its workspace members.
|
||||
MismatchedSources(PackageName, bool),
|
||||
/// A workspace member switched from virtual to non-virtual or vice versa.
|
||||
MismatchedVirtual(PackageName, bool),
|
||||
/// A workspace member switched from dynamic to non-dynamic or vice versa.
|
||||
MismatchedDynamic(PackageName, bool),
|
||||
/// The lockfile uses a different set of version for its workspace members.
|
||||
MismatchedVersion(PackageName, Version, Option<Version>),
|
||||
/// The lockfile uses a different set of requirements.
|
||||
|
@ -1432,14 +1421,14 @@ pub enum SatisfiesResult<'lock> {
|
|||
/// A package in the lockfile contains different `requires-dist` metadata than expected.
|
||||
MismatchedPackageRequirements(
|
||||
&'lock PackageName,
|
||||
&'lock Version,
|
||||
Option<&'lock Version>,
|
||||
BTreeSet<Requirement>,
|
||||
BTreeSet<Requirement>,
|
||||
),
|
||||
/// A package in the lockfile contains different `dependency-group` metadata than expected.
|
||||
MismatchedPackageDependencyGroups(
|
||||
&'lock PackageName,
|
||||
&'lock Version,
|
||||
Option<&'lock Version>,
|
||||
BTreeMap<GroupName, BTreeSet<Requirement>>,
|
||||
BTreeMap<GroupName, BTreeSet<Requirement>>,
|
||||
),
|
||||
|
@ -1953,7 +1942,7 @@ impl Package {
|
|||
let install_path = absolute_path(workspace_root, path)?;
|
||||
let path_dist = PathSourceDist {
|
||||
name: self.id.name.clone(),
|
||||
version: Some(self.id.version.clone()),
|
||||
version: self.id.version.clone(),
|
||||
url: verbatim_url(&install_path, &self.id)?,
|
||||
install_path,
|
||||
ext,
|
||||
|
@ -2047,9 +2036,16 @@ impl Package {
|
|||
return Ok(None);
|
||||
};
|
||||
|
||||
let name = &self.id.name;
|
||||
let version = self
|
||||
.id
|
||||
.version
|
||||
.as_ref()
|
||||
.expect("version for registry source");
|
||||
|
||||
let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
|
||||
name: self.id.name.clone(),
|
||||
version: self.id.version.clone(),
|
||||
name: name.clone(),
|
||||
version: version.clone(),
|
||||
})?;
|
||||
let filename = sdist
|
||||
.filename()
|
||||
|
@ -2076,8 +2072,8 @@ impl Package {
|
|||
));
|
||||
|
||||
let reg_dist = RegistrySourceDist {
|
||||
name: self.id.name.clone(),
|
||||
version: self.id.version.clone(),
|
||||
name: name.clone(),
|
||||
version: version.clone(),
|
||||
file,
|
||||
ext,
|
||||
index,
|
||||
|
@ -2090,9 +2086,16 @@ impl Package {
|
|||
return Ok(None);
|
||||
};
|
||||
|
||||
let name = &self.id.name;
|
||||
let version = self
|
||||
.id
|
||||
.version
|
||||
.as_ref()
|
||||
.expect("version for registry source");
|
||||
|
||||
let file_path = sdist.path().ok_or_else(|| LockErrorKind::MissingPath {
|
||||
name: self.id.name.clone(),
|
||||
version: self.id.version.clone(),
|
||||
name: name.clone(),
|
||||
version: version.clone(),
|
||||
})?;
|
||||
let file_url = Url::from_file_path(workspace_root.join(path).join(file_path))
|
||||
.map_err(|()| LockErrorKind::PathToUrl)?;
|
||||
|
@ -2121,8 +2124,8 @@ impl Package {
|
|||
);
|
||||
|
||||
let reg_dist = RegistrySourceDist {
|
||||
name: self.id.name.clone(),
|
||||
version: self.id.version.clone(),
|
||||
name: name.clone(),
|
||||
version: version.clone(),
|
||||
file,
|
||||
ext,
|
||||
index,
|
||||
|
@ -2302,8 +2305,8 @@ impl Package {
|
|||
}
|
||||
|
||||
/// Returns the [`Version`] of the package.
|
||||
pub fn version(&self) -> &Version {
|
||||
&self.id.version
|
||||
pub fn version(&self) -> Option<&Version> {
|
||||
self.id.version.as_ref()
|
||||
}
|
||||
|
||||
/// Return the fork markers for this package, if any.
|
||||
|
@ -2358,6 +2361,11 @@ impl Package {
|
|||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the package is a dynamic source tree.
|
||||
fn is_dynamic(&self) -> bool {
|
||||
self.id.version.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to construct a `VerbatimUrl` from the given `Path`.
|
||||
|
@ -2452,7 +2460,7 @@ impl PackageWire {
|
|||
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
|
||||
pub(crate) struct PackageId {
|
||||
pub(crate) name: PackageName,
|
||||
pub(crate) version: Version,
|
||||
pub(crate) version: Option<Version>,
|
||||
source: Source,
|
||||
}
|
||||
|
||||
|
@ -2461,9 +2469,20 @@ impl PackageId {
|
|||
annotated_dist: &AnnotatedDist,
|
||||
root: &Path,
|
||||
) -> Result<PackageId, LockError> {
|
||||
let name = annotated_dist.name.clone();
|
||||
let version = annotated_dist.version.clone();
|
||||
// Identify the source of the package.
|
||||
let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
|
||||
// Omit versions for dynamic source trees.
|
||||
let version = if source.is_source_tree()
|
||||
&& annotated_dist
|
||||
.metadata
|
||||
.as_ref()
|
||||
.is_some_and(|metadata| metadata.dynamic)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(annotated_dist.version.clone())
|
||||
};
|
||||
let name = annotated_dist.name.clone();
|
||||
Ok(Self {
|
||||
name,
|
||||
version,
|
||||
|
@ -2481,7 +2500,9 @@ impl PackageId {
|
|||
let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
|
||||
table.insert("name", value(self.name.to_string()));
|
||||
if count.map(|count| count > 1).unwrap_or(true) {
|
||||
table.insert("version", value(self.version.to_string()));
|
||||
if let Some(version) = &self.version {
|
||||
table.insert("version", value(version.to_string()));
|
||||
}
|
||||
self.source.to_toml(table);
|
||||
}
|
||||
}
|
||||
|
@ -2489,7 +2510,11 @@ impl PackageId {
|
|||
|
||||
impl Display for PackageId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}=={} @ {}", self.name, self.version, self.source)
|
||||
if let Some(version) = &self.version {
|
||||
write!(f, "{}=={} @ {}", self.name, version, self.source)
|
||||
} else {
|
||||
write!(f, "{} @ {}", self.name, self.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2506,15 +2531,17 @@ impl PackageIdForDependency {
|
|||
unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
|
||||
) -> Result<PackageId, LockError> {
|
||||
let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
|
||||
let version = self.version.map(Ok::<_, LockError>).unwrap_or_else(|| {
|
||||
let version = if let Some(version) = self.version {
|
||||
Some(version)
|
||||
} else {
|
||||
let Some(dist_id) = unambiguous_package_id else {
|
||||
return Err(LockErrorKind::MissingDependencyVersion {
|
||||
name: self.name.clone(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
Ok(dist_id.version.clone())
|
||||
})?;
|
||||
dist_id.version.clone()
|
||||
};
|
||||
let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
|
||||
let Some(package_id) = unambiguous_package_id else {
|
||||
return Err(LockErrorKind::MissingDependencySource {
|
||||
|
@ -2536,7 +2563,7 @@ impl From<PackageId> for PackageIdForDependency {
|
|||
fn from(id: PackageId) -> PackageIdForDependency {
|
||||
PackageIdForDependency {
|
||||
name: id.name,
|
||||
version: Some(id.version),
|
||||
version: id.version,
|
||||
source: Some(id.source),
|
||||
}
|
||||
}
|
||||
|
@ -2742,6 +2769,14 @@ impl Source {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the source is that of a source tree.
|
||||
pub(crate) fn is_source_tree(&self) -> bool {
|
||||
match self {
|
||||
Source::Directory(..) | Source::Editable(..) | Source::Virtual(..) => true,
|
||||
Source::Path(..) | Source::Git(..) | Source::Registry(..) | Source::Direct(..) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_toml(&self, table: &mut Table) {
|
||||
let mut source_table = InlineTable::new();
|
||||
match *self {
|
||||
|
@ -3824,21 +3859,22 @@ impl Dependency {
|
|||
|
||||
impl Display for Dependency {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if self.extra.is_empty() {
|
||||
write!(
|
||||
match (self.extra.is_empty(), self.package_id.version.as_ref()) {
|
||||
(true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
|
||||
(true, None) => write!(f, "{}", self.package_id.name),
|
||||
(false, Some(version)) => write!(
|
||||
f,
|
||||
"{}=={} @ {}",
|
||||
self.package_id.name, self.package_id.version, self.package_id.source
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}[{}]=={} @ {}",
|
||||
"{}[{}]=={}",
|
||||
self.package_id.name,
|
||||
self.extra.iter().join(","),
|
||||
self.package_id.version,
|
||||
self.package_id.source
|
||||
)
|
||||
version
|
||||
),
|
||||
(false, None) => write!(
|
||||
f,
|
||||
"{}[{}]",
|
||||
self.package_id.name,
|
||||
self.extra.iter().join(",")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -298,7 +298,12 @@ impl std::fmt::Display for RequirementsTxtExport<'_> {
|
|||
for Requirement { package, marker } in &self.nodes {
|
||||
match &package.id.source {
|
||||
Source::Registry(_) => {
|
||||
write!(f, "{}=={}", package.id.name, package.id.version)?;
|
||||
let version = package
|
||||
.id
|
||||
.version
|
||||
.as_ref()
|
||||
.expect("registry package without version");
|
||||
write!(f, "{}=={}", package.id.name, version)?;
|
||||
}
|
||||
Source::Git(url, git) => {
|
||||
// Remove the fragment and query from the URL; they're already present in the
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -94,7 +96,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -101,7 +103,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Path(
|
||||
"file:///foo/bar",
|
||||
),
|
||||
|
@ -97,7 +99,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Path(
|
||||
"file:///foo/bar",
|
||||
),
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -86,7 +88,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"b",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -123,7 +127,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -148,24 +154,13 @@ Ok(
|
|||
},
|
||||
],
|
||||
by_id: {
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"b",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -174,6 +169,21 @@ Ok(
|
|||
),
|
||||
),
|
||||
}: 1,
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
},
|
||||
manifest: ResolverManifest {
|
||||
members: {},
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -86,7 +88,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"b",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -123,7 +127,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -148,24 +154,13 @@ Ok(
|
|||
},
|
||||
],
|
||||
by_id: {
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"b",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -174,6 +169,21 @@ Ok(
|
|||
),
|
||||
),
|
||||
}: 1,
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
},
|
||||
manifest: ResolverManifest {
|
||||
members: {},
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -86,7 +88,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"b",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -123,7 +127,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -148,24 +154,13 @@ Ok(
|
|||
},
|
||||
],
|
||||
by_id: {
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"b",
|
||||
),
|
||||
version: "0.1.0",
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
|
@ -174,6 +169,21 @@ Ok(
|
|||
),
|
||||
),
|
||||
}: 1,
|
||||
PackageId {
|
||||
name: PackageName(
|
||||
"a",
|
||||
),
|
||||
version: Some(
|
||||
"0.1.0",
|
||||
),
|
||||
source: Registry(
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
},
|
||||
manifest: ResolverManifest {
|
||||
members: {},
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Direct(
|
||||
UrlString(
|
||||
"https://burntsushi.net",
|
||||
|
@ -71,7 +73,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Direct(
|
||||
UrlString(
|
||||
"https://burntsushi.net",
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Direct(
|
||||
UrlString(
|
||||
"https://burntsushi.net",
|
||||
|
@ -69,7 +71,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Direct(
|
||||
UrlString(
|
||||
"https://burntsushi.net",
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Directory(
|
||||
"path/to/dir",
|
||||
),
|
||||
|
@ -64,7 +66,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Directory(
|
||||
"path/to/dir",
|
||||
),
|
||||
|
|
|
@ -42,7 +42,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Editable(
|
||||
"path/to/dir",
|
||||
),
|
||||
|
@ -64,7 +66,9 @@ Ok(
|
|||
name: PackageName(
|
||||
"anyio",
|
||||
),
|
||||
version: "4.3.0",
|
||||
version: Some(
|
||||
"4.3.0",
|
||||
),
|
||||
source: Editable(
|
||||
"path/to/dir",
|
||||
),
|
||||
|
|
|
@ -369,9 +369,11 @@ impl<'env> TreeDisplay<'env> {
|
|||
}
|
||||
}
|
||||
|
||||
line.push(' ');
|
||||
line.push('v');
|
||||
line.push_str(&format!("{}", package_id.version));
|
||||
if let Some(version) = package_id.version.as_ref() {
|
||||
line.push(' ');
|
||||
line.push('v');
|
||||
line.push_str(&format!("{version}"));
|
||||
}
|
||||
|
||||
if let Some(edge) = edge {
|
||||
match edge {
|
||||
|
|
|
@ -95,15 +95,18 @@ impl Preference {
|
|||
pub fn from_lock(
|
||||
package: &crate::lock::Package,
|
||||
install_path: &Path,
|
||||
) -> Result<Self, LockError> {
|
||||
Ok(Self {
|
||||
) -> Result<Option<Self>, LockError> {
|
||||
let Some(version) = package.version() else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Self {
|
||||
name: package.id.name.clone(),
|
||||
version: package.id.version.clone(),
|
||||
version: version.clone(),
|
||||
marker: MarkerTree::TRUE,
|
||||
index: package.index(install_path)?,
|
||||
fork_markers: package.fork_markers().to_vec(),
|
||||
hashes: Vec::new(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return the [`PackageName`] of the package for this [`Preference`].
|
||||
|
|
|
@ -87,6 +87,13 @@ impl PyProjectToml {
|
|||
self.build_system.is_some()
|
||||
}
|
||||
|
||||
/// Returns `true` if the project uses a dynamic version.
|
||||
pub fn is_dynamic(&self) -> bool {
|
||||
self.project
|
||||
.as_ref()
|
||||
.is_some_and(|project| project.version.is_none())
|
||||
}
|
||||
|
||||
/// Returns whether the project manifest contains any script table.
|
||||
pub fn has_scripts(&self) -> bool {
|
||||
if let Some(ref project) = self.project {
|
||||
|
|
|
@ -690,7 +690,9 @@ async fn lock_and_sync(
|
|||
FxHashMap::with_capacity_and_hasher(lock.packages().len(), FxBuildHasher);
|
||||
for dist in lock.packages() {
|
||||
let name = dist.name();
|
||||
let version = dist.version();
|
||||
let Some(version) = dist.version() else {
|
||||
continue;
|
||||
};
|
||||
match minimum_version.entry(name) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(version);
|
||||
|
|
|
@ -930,7 +930,7 @@ impl ValidatedLock {
|
|||
);
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MismatchedSources(name, expected) => {
|
||||
SatisfiesResult::MismatchedVirtual(name, expected) => {
|
||||
if expected {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to mismatched source: `{name}` (expected: `virtual`)"
|
||||
|
@ -942,6 +942,18 @@ impl ValidatedLock {
|
|||
}
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MismatchedDynamic(name, expected) => {
|
||||
if expected {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to static version: `{name}` (expected a dynamic version)"
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to dynamic version: `{name}` (expected a static version)"
|
||||
);
|
||||
}
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MismatchedVersion(name, expected, actual) => {
|
||||
if let Some(actual) = actual {
|
||||
debug!(
|
||||
|
@ -1006,17 +1018,31 @@ impl ValidatedLock {
|
|||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MismatchedPackageRequirements(name, version, expected, actual) => {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to mismatched `requires-dist` for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
|
||||
expected, actual
|
||||
);
|
||||
if let Some(version) = version {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to mismatched requirements for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
|
||||
expected, actual
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to mismatched requirements for: `{name}`\n Requested: {:?}\n Existing: {:?}",
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MismatchedPackageDependencyGroups(name, version, expected, actual) => {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to mismatched dev dependencies for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
|
||||
expected, actual
|
||||
);
|
||||
if let Some(version) = version {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to mismatched dependency groups for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
|
||||
expected, actual
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Ignoring existing lockfile due to mismatched dependency groups for: `{name}`\n Requested: {:?}\n Existing: {:?}",
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
}
|
||||
|
@ -1048,9 +1074,9 @@ fn report_upgrades(
|
|||
existing_lock.packages().iter().fold(
|
||||
FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher),
|
||||
|mut acc, package| {
|
||||
acc.entry(package.name())
|
||||
.or_default()
|
||||
.insert(package.version());
|
||||
if let Some(version) = package.version() {
|
||||
acc.entry(package.name()).or_default().insert(version);
|
||||
}
|
||||
acc
|
||||
},
|
||||
)
|
||||
|
@ -1062,9 +1088,9 @@ fn report_upgrades(
|
|||
new_lock.packages().iter().fold(
|
||||
FxHashMap::with_capacity_and_hasher(new_lock.packages().len(), FxBuildHasher),
|
||||
|mut acc, package| {
|
||||
acc.entry(package.name())
|
||||
.or_default()
|
||||
.insert(package.version());
|
||||
if let Some(version) = package.version() {
|
||||
acc.entry(package.name()).or_default().insert(version);
|
||||
}
|
||||
acc
|
||||
},
|
||||
);
|
||||
|
|
|
@ -257,7 +257,7 @@ pub(crate) async fn tree(
|
|||
continue;
|
||||
};
|
||||
reporter.on_fetch_version(package.name(), &version);
|
||||
if version > *package.version() {
|
||||
if package.version().is_some_and(|package| version > *package) {
|
||||
map.insert(package.clone(), version);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14856,7 +14856,7 @@ fn lock_explicit_default_index() -> Result<()> {
|
|||
DEBUG Using request timeout of [TIME]
|
||||
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
|
||||
DEBUG No workspace root found, using project root
|
||||
DEBUG Ignoring existing lockfile due to mismatched `requires-dist` for: `project==0.1.0`
|
||||
DEBUG Ignoring existing lockfile due to mismatched requirements for: `project==0.1.0`
|
||||
Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }}
|
||||
Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }), conflict: None }, origin: None }}
|
||||
DEBUG Solving with installed Python version: 3.12.[X]
|
||||
|
@ -19821,7 +19821,7 @@ fn lock_transitive_git() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock a package that's excluded from the parent workspace, but depends on that parent.
|
||||
/// Lock a package with a dynamic version.
|
||||
#[test]
|
||||
fn lock_dynamic_version() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
@ -19883,7 +19883,6 @@ fn lock_dynamic_version() -> Result<()> {
|
|||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
"###
|
||||
);
|
||||
|
@ -19897,26 +19896,14 @@ fn lock_dynamic_version() -> Result<()> {
|
|||
.child("__init__.py")
|
||||
.write_str("__version__ = '0.1.1'")?;
|
||||
|
||||
// Re-run with `--locked`.
|
||||
// Re-run with `--locked`. We should accept the lockfile, since dynamic versions are omitted.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
"###);
|
||||
|
||||
// Re-lock.
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Updated project v0.1.0 -> v0.1.1
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
@ -19934,7 +19921,6 @@ fn lock_dynamic_version() -> Result<()> {
|
|||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.1"
|
||||
source = { editable = "." }
|
||||
"###
|
||||
);
|
||||
|
@ -19943,6 +19929,368 @@ fn lock_dynamic_version() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock a package that depends on a package with a dynamic version using a `workspace` source.
|
||||
#[test]
|
||||
fn lock_dynamic_version_workspace_member() -> 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 = ["dynamic", "iniconfig>=2"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["dynamic"]
|
||||
|
||||
[tool.uv.sources]
|
||||
dynamic = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Create a project with a dynamic version.
|
||||
let pyproject_toml = context.temp_dir.child("dynamic").child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "dynamic"
|
||||
requires-python = ">=3.12"
|
||||
dynamic = ["version"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.uv]
|
||||
cache-keys = [{ file = "pyproject.toml" }, { file = "src/dynamic/__init__.py" }]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = { attr = "dynamic.__version__" }
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = { "" = "src" }
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("dynamic")
|
||||
.child("src")
|
||||
.child("dynamic")
|
||||
.child("__init__.py")
|
||||
.write_str("__version__ = '0.1.0'")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"dynamic",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynamic"
|
||||
source = { editable = "dynamic" }
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dynamic" },
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dynamic", editable = "dynamic" },
|
||||
{ name = "iniconfig", specifier = ">=2" },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Bump the version.
|
||||
context
|
||||
.temp_dir
|
||||
.child("dynamic")
|
||||
.child("src")
|
||||
.child("dynamic")
|
||||
.child("__init__.py")
|
||||
.write_str("__version__ = '0.1.1'")?;
|
||||
|
||||
// Re-run with `--locked`. We should accept the lockfile, since dynamic versions are omitted.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"dynamic",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynamic"
|
||||
source = { editable = "dynamic" }
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dynamic" },
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dynamic", editable = "dynamic" },
|
||||
{ name = "iniconfig", specifier = ">=2" },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock a package that depends on a package with a dynamic version using a `path` source (as
|
||||
/// opposed to a workspace).
|
||||
#[test]
|
||||
fn lock_dynamic_version_path_dependency() -> 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 = ["dynamic", "iniconfig>=2"]
|
||||
|
||||
[tool.uv.sources]
|
||||
dynamic = { path = "dynamic" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Create a project with a dynamic version.
|
||||
let pyproject_toml = context.temp_dir.child("dynamic").child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "dynamic"
|
||||
requires-python = ">=3.12"
|
||||
dynamic = ["version"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.uv]
|
||||
cache-keys = [{ file = "pyproject.toml" }, { file = "src/dynamic/__init__.py" }]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = { attr = "dynamic.__version__" }
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = { "" = "src" }
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("dynamic")
|
||||
.child("src")
|
||||
.child("dynamic")
|
||||
.child("__init__.py")
|
||||
.write_str("__version__ = '0.1.0'")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "dynamic"
|
||||
source = { directory = "dynamic" }
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dynamic" },
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dynamic", directory = "dynamic" },
|
||||
{ name = "iniconfig", specifier = ">=2" },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Bump the version.
|
||||
context
|
||||
.temp_dir
|
||||
.child("dynamic")
|
||||
.child("src")
|
||||
.child("dynamic")
|
||||
.child("__init__.py")
|
||||
.write_str("__version__ = '0.1.1'")?;
|
||||
|
||||
// Re-run with `--locked`. We should accept the lockfile, since dynamic versions are omitted.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "dynamic"
|
||||
source = { directory = "dynamic" }
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dynamic" },
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dynamic", directory = "dynamic" },
|
||||
{ name = "iniconfig", specifier = ">=2" },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_derivation_chain_prod() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
@ -179,7 +179,6 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.8.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue