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:
Charlie Marsh 2025-01-15 11:54:32 -05:00 committed by GitHub
parent d20a48a5b4
commit 0617fd5da6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 990 additions and 366 deletions

View file

@ -51,6 +51,7 @@ impl DependencyMetadata {
requires_dist: metadata.requires_dist.clone(), requires_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(), requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(), provides_extras: metadata.provides_extras.clone(),
dynamic: false,
}) })
} else { } else {
// If no version was requested (i.e., it's a direct URL dependency), allow a single // 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_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(), requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(), provides_extras: metadata.provides_extras.clone(),
dynamic: false,
}) })
} }
} }

View file

@ -50,6 +50,7 @@ pub struct Metadata {
pub requires_python: Option<VersionSpecifiers>, pub requires_python: Option<VersionSpecifiers>,
pub provides_extras: Vec<ExtraName>, pub provides_extras: Vec<ExtraName>,
pub dependency_groups: BTreeMap<GroupName, Vec<uv_pypi_types::Requirement>>, pub dependency_groups: BTreeMap<GroupName, Vec<uv_pypi_types::Requirement>>,
pub dynamic: bool,
} }
impl Metadata { impl Metadata {
@ -67,6 +68,7 @@ impl Metadata {
requires_python: metadata.requires_python, requires_python: metadata.requires_python,
provides_extras: metadata.provides_extras, provides_extras: metadata.provides_extras,
dependency_groups: BTreeMap::default(), dependency_groups: BTreeMap::default(),
dynamic: metadata.dynamic,
} }
} }
@ -109,6 +111,7 @@ impl Metadata {
requires_python: metadata.requires_python, requires_python: metadata.requires_python,
provides_extras, provides_extras,
dependency_groups, dependency_groups,
dynamic: metadata.dynamic,
}) })
} }
} }

View file

@ -535,14 +535,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
} }
// If the metadata is static, return it. // If the metadata is static, return it.
if let Some(metadata) = let dynamic =
Self::read_static_metadata(source, source_dist_entry.path(), subdirectory).await? match StaticMetadata::read(source, source_dist_entry.path(), subdirectory).await? {
{ StaticMetadata::Some(metadata) => {
return Ok(ArchiveMetadata { return Ok(ArchiveMetadata {
metadata: Metadata::from_metadata23(metadata), metadata: Metadata::from_metadata23(metadata),
hashes: revision.into_hashes(), hashes: revision.into_hashes(),
}); });
} }
StaticMetadata::Dynamic => true,
StaticMetadata::None => false,
};
// If the cache contains compatible metadata, return it. // If the cache contains compatible metadata, return it.
let metadata_entry = cache_shard.entry(METADATA); let metadata_entry = cache_shard.entry(METADATA);
@ -593,6 +596,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
{ {
// If necessary, mark the metadata as dynamic.
let metadata = if dynamic {
ResolutionMetadata {
dynamic: true,
..metadata
}
} else {
metadata
};
// Store the metadata. // Store the metadata.
fs::create_dir_all(metadata_entry.dir()) fs::create_dir_all(metadata_entry.dir())
.await .await
@ -623,17 +636,27 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
) )
.await?; .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(task) = task {
if let Some(reporter) = self.reporter.as_ref() { if let Some(reporter) = self.reporter.as_ref() {
reporter.on_build_complete(source, task); 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 { Ok(ArchiveMetadata {
metadata: Metadata::from_metadata23(metadata), metadata: Metadata::from_metadata23(metadata),
hashes: revision.into_hashes(), hashes: revision.into_hashes(),
@ -844,14 +867,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let source_entry = cache_shard.entry(SOURCE); let source_entry = cache_shard.entry(SOURCE);
// If the metadata is static, return it. // If the metadata is static, return it.
if let Some(metadata) = let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? {
Self::read_static_metadata(source, source_entry.path(), None).await? StaticMetadata::Some(metadata) => {
{ return Ok(ArchiveMetadata {
return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata),
metadata: Metadata::from_metadata23(metadata), hashes: revision.into_hashes(),
hashes: revision.into_hashes(), });
}); }
} StaticMetadata::Dynamic => true,
StaticMetadata::None => false,
};
// If the cache contains compatible metadata, return it. // If the cache contains compatible metadata, return it.
let metadata_entry = cache_shard.entry(METADATA); let metadata_entry = cache_shard.entry(METADATA);
@ -880,6 +905,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
{ {
// If necessary, mark the metadata as dynamic.
let metadata = if dynamic {
ResolutionMetadata {
dynamic: true,
..metadata
}
} else {
metadata
};
// Store the metadata. // Store the metadata.
fs::create_dir_all(metadata_entry.dir()) fs::create_dir_all(metadata_entry.dir())
.await .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. // Store the metadata.
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
.await .await
@ -1093,21 +1138,24 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
return Err(Error::HashesNotSupportedSourceTree(source.to_string())); return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
} }
if let Some(metadata) = // If the metadata is static, return it.
Self::read_static_metadata(source, &resource.install_path, None).await? let dynamic = match StaticMetadata::read(source, &resource.install_path, None).await? {
{ StaticMetadata::Some(metadata) => {
return Ok(ArchiveMetadata::from( return Ok(ArchiveMetadata::from(
Metadata::from_workspace( Metadata::from_workspace(
metadata, metadata,
resource.install_path.as_ref(), resource.install_path.as_ref(),
None, None,
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.bounds(), self.build_context.bounds(),
) )
.await?, .await?,
)); ));
} }
StaticMetadata::Dynamic => true,
StaticMetadata::None => false,
};
let cache_shard = self.build_context.cache().shard( let cache_shard = self.build_context.cache().shard(
CacheBucket::SourceDistributions, CacheBucket::SourceDistributions,
@ -1160,6 +1208,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
{ {
// If necessary, mark the metadata as dynamic.
let metadata = if dynamic {
ResolutionMetadata {
dynamic: true,
..metadata
}
} else {
metadata
};
// Store the metadata. // Store the metadata.
fs::create_dir_all(metadata_entry.dir()) fs::create_dir_all(metadata_entry.dir())
.await .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. // Store the metadata.
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
.await .await
@ -1472,21 +1540,25 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
git_source: resource, git_source: resource,
}; };
if let Some(metadata) = // If the metadata is static, return it.
Self::read_static_metadata(source, fetch.path(), resource.subdirectory).await? let dynamic =
{ match StaticMetadata::read(source, fetch.path(), resource.subdirectory).await? {
return Ok(ArchiveMetadata::from( StaticMetadata::Some(metadata) => {
Metadata::from_workspace( return Ok(ArchiveMetadata::from(
metadata, Metadata::from_workspace(
&path, metadata,
Some(&git_member), &path,
self.build_context.locations(), Some(&git_member),
self.build_context.sources(), self.build_context.locations(),
self.build_context.bounds(), self.build_context.sources(),
) self.build_context.bounds(),
.await?, )
)); .await?,
} ));
}
StaticMetadata::Dynamic => true,
StaticMetadata::None => false,
};
// If the cache contains compatible metadata, return it. // If the cache contains compatible metadata, return it.
if self if self
@ -1531,6 +1603,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.await? .await?
{ {
// If necessary, mark the metadata as dynamic.
let metadata = if dynamic {
ResolutionMetadata {
dynamic: true,
..metadata
}
} else {
metadata
};
// Store the metadata. // Store the metadata.
fs::create_dir_all(metadata_entry.dir()) fs::create_dir_all(metadata_entry.dir())
.await .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. // Store the metadata.
write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?)
.await .await
@ -2025,122 +2117,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Ok(Some(metadata)) 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. /// Returns a GET [`reqwest::Request`] for the given URL.
fn request(url: Url, client: &RegistryClient) -> Result<reqwest::Request, reqwest::Error> { fn request(url: Url, client: &RegistryClient) -> Result<reqwest::Request, reqwest::Error> {
client client
@ -2225,6 +2201,146 @@ pub fn prune(cache: &Cache) -> Result<Removal, Error> {
Ok(removal) 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. /// Validate that the source distribution matches the built metadata.
fn validate_metadata( fn validate_metadata(
source: &BuildableSource<'_>, source: &BuildableSource<'_>,
@ -2464,6 +2580,9 @@ async fn read_egg_info(
// Parse the metadata. // Parse the metadata.
let metadata = Metadata12::parse_metadata(&content).map_err(Error::PkgInfo)?; 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. // Combine the sources.
Ok(ResolutionMetadata { Ok(ResolutionMetadata {
name: metadata.name, name: metadata.name,
@ -2471,6 +2590,7 @@ async fn read_egg_info(
requires_python: metadata.requires_python, requires_python: metadata.requires_python,
requires_dist: requires_txt.requires_dist, requires_dist: requires_txt.requires_dist,
provides_extras: requires_txt.provides_extras, provides_extras: requires_txt.provides_extras,
dynamic,
}) })
} }

View file

@ -6,7 +6,8 @@ use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifiers}; use uv_pep440::{Version, VersionSpecifiers};
/// A subset of the full cure metadata specification, only including the /// 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/>. /// Python Package Metadata 1.2 is specified in <https://peps.python.org/pep-0345/>.
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@ -15,6 +16,7 @@ pub struct Metadata12 {
pub name: PackageName, pub name: PackageName,
pub version: Version, pub version: Version,
pub requires_python: Option<VersionSpecifiers>, pub requires_python: Option<VersionSpecifiers>,
pub dynamic: Vec<String>,
} }
impl Metadata12 { impl Metadata12 {
@ -54,11 +56,13 @@ impl Metadata12 {
.map(|requires_python| LenientVersionSpecifiers::from_str(&requires_python)) .map(|requires_python| LenientVersionSpecifiers::from_str(&requires_python))
.transpose()? .transpose()?
.map(VersionSpecifiers::from); .map(VersionSpecifiers::from);
let dynamic = headers.get_all_values("Dynamic").collect::<Vec<_>>();
Ok(Self { Ok(Self {
name, name,
version, version,
requires_python, requires_python,
dynamic,
}) })
} }
} }

View file

@ -29,6 +29,9 @@ pub struct ResolutionMetadata {
pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>, pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>,
pub requires_python: Option<VersionSpecifiers>, pub requires_python: Option<VersionSpecifiers>,
pub provides_extras: Vec<ExtraName>, 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> /// From <https://github.com/PyO3/python-pkginfo-rs/blob/d719988323a0cfea86d4737116d7917f30e819e2/src/metadata.rs#LL78C2-L91C26>
@ -68,6 +71,9 @@ impl ResolutionMetadata {
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let dynamic = headers
.get_all_values("Dynamic")
.any(|field| field == "Version");
Ok(Self { Ok(Self {
name, name,
@ -75,6 +81,7 @@ impl ResolutionMetadata {
requires_dist, requires_dist,
requires_python, requires_python,
provides_extras, 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. // 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<_>>(); let mut dynamic = false;
for field in dynamic { for field in headers.get_all_values("Dynamic") {
match field.as_str() { match field.as_str() {
"Requires-Python" => return Err(MetadataError::DynamicField("Requires-Python")), "Requires-Python" => return Err(MetadataError::DynamicField("Requires-Python")),
"Requires-Dist" => return Err(MetadataError::DynamicField("Requires-Dist")), "Requires-Dist" => return Err(MetadataError::DynamicField("Requires-Dist")),
"Provides-Extra" => return Err(MetadataError::DynamicField("Provides-Extra")), "Provides-Extra" => return Err(MetadataError::DynamicField("Provides-Extra")),
"Version" => dynamic = true,
_ => (), _ => (),
} }
} }
@ -148,6 +156,7 @@ impl ResolutionMetadata {
requires_dist, requires_dist,
requires_python, requires_python,
provides_extras, provides_extras,
dynamic,
}) })
} }

View file

@ -29,8 +29,8 @@ pub(crate) fn parse_pyproject_toml(
.ok_or(MetadataError::FieldNotFound("project"))?; .ok_or(MetadataError::FieldNotFound("project"))?;
// If any of the fields we need were declared as dynamic, we can't use the `pyproject.toml` file. // 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(); let mut dynamic = false;
for field in dynamic { for field in project.dynamic.unwrap_or_default() {
match field.as_str() { match field.as_str() {
"dependencies" => return Err(MetadataError::DynamicField("dependencies")), "dependencies" => return Err(MetadataError::DynamicField("dependencies")),
"optional-dependencies" => { "optional-dependencies" => {
@ -39,8 +39,11 @@ pub(crate) fn parse_pyproject_toml(
"requires-python" => return Err(MetadataError::DynamicField("requires-python")), "requires-python" => return Err(MetadataError::DynamicField("requires-python")),
// When building from a source distribution, the version is known from the filename and // When building from a source distribution, the version is known from the filename and
// fixed by it, so we can pretend it's static. // fixed by it, so we can pretend it's static.
"version" if sdist_version.is_none() => { "version" => {
return Err(MetadataError::DynamicField("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_dist,
requires_python, requires_python,
provides_extras, provides_extras,
dynamic,
}) })
} }

View file

@ -83,7 +83,9 @@ pub fn read_lock_requirements(
} }
// Map each entry in the lockfile to a preference. // 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. // Map each entry in the lockfile to a Git SHA.
if let Some(git_ref) = package.as_git_ref()? { if let Some(git_ref) = package.as_git_ref()? {

View file

@ -344,11 +344,8 @@ pub trait Installable<'lock> {
TagPolicy::Required(tags), TagPolicy::Required(tags),
build_options, build_options,
)?; )?;
let version = package.version().clone(); let version = package.version().cloned();
let dist = ResolvedDist::Installable { let dist = ResolvedDist::Installable { dist, version };
dist,
version: Some(version),
};
let hashes = package.hashes(); let hashes = package.hashes();
Ok(Node::Dist { Ok(Node::Dist {
dist, dist,
@ -364,11 +361,8 @@ pub trait Installable<'lock> {
TagPolicy::Preferred(tags), TagPolicy::Preferred(tags),
&BuildOptions::default(), &BuildOptions::default(),
)?; )?;
let version = package.version().clone(); let version = package.version().cloned();
let dist = ResolvedDist::Installable { let dist = ResolvedDist::Installable { dist, version };
dist,
version: Some(version),
};
let hashes = package.hashes(); let hashes = package.hashes();
Ok(Node::Dist { Ok(Node::Dist {
dist, dist,

View file

@ -1004,31 +1004,20 @@ impl Lock {
.flatten() .flatten()
.map(|package| matches!(package.id.source, Source::Virtual(_))); .map(|package| matches!(package.id.source, Source::Virtual(_)));
if actual != Some(expected) { 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 { for (name, member) in packages {
let Some(expected) = member let expected = member.pyproject_toml().is_dynamic();
.pyproject_toml()
.project
.as_ref()
.and_then(|project| project.version.as_ref())
else {
continue;
};
let actual = self let actual = self
.find_by_name(name) .find_by_name(name)
.ok() .ok()
.flatten() .flatten()
.map(|package| &package.id.version); .map(Package::is_dynamic);
if actual != Some(expected) { if actual != Some(expected) {
return Ok(SatisfiesResult::MismatchedVersion( return Ok(SatisfiesResult::MismatchedDynamic(name.clone(), expected));
name.clone(),
expected.clone(),
actual.cloned(),
));
} }
} }
} }
@ -1196,20 +1185,24 @@ impl Lock {
.as_ref() .as_ref()
.is_some_and(|remotes| !remotes.contains(url)) .is_some_and(|remotes| !remotes.contains(url))
{ {
return Ok(SatisfiesResult::MissingRemoteIndex( let name = &package.id.name;
&package.id.name, let version = &package
&package.id.version, .id
url, .version
)); .as_ref()
.expect("version for registry source");
return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
} }
} }
RegistrySource::Path(path) => { RegistrySource::Path(path) => {
if locals.as_ref().is_some_and(|locals| !locals.contains(path)) { if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
return Ok(SatisfiesResult::MissingLocalIndex( let name = &package.id.name;
&package.id.name, let version = &package
&package.id.version, .id
path, .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. // 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 metadata = {
let id = dist.version_id(); let id = dist.version_id();
if let Some(archive) = 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. // Validate the `requires-dist` metadata.
{ {
let expected: BTreeSet<_> = metadata let expected: BTreeSet<_> = metadata
@ -1298,7 +1285,7 @@ impl Lock {
if expected != actual { if expected != actual {
return Ok(SatisfiesResult::MismatchedPackageRequirements( return Ok(SatisfiesResult::MismatchedPackageRequirements(
&package.id.name, &package.id.name,
&package.id.version, package.id.version.as_ref(),
expected, expected,
actual, actual,
)); ));
@ -1341,7 +1328,7 @@ impl Lock {
if expected != actual { if expected != actual {
return Ok(SatisfiesResult::MismatchedPackageDependencyGroups( return Ok(SatisfiesResult::MismatchedPackageDependencyGroups(
&package.id.name, &package.id.name,
&package.id.version, package.id.version.as_ref(),
expected, expected,
actual, actual,
)); ));
@ -1406,8 +1393,10 @@ pub enum SatisfiesResult<'lock> {
Satisfied, Satisfied,
/// The lockfile uses a different set of workspace members. /// The lockfile uses a different set of workspace members.
MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>), MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
/// The lockfile uses a different set of sources for its workspace members. /// A workspace member switched from virtual to non-virtual or vice versa.
MismatchedSources(PackageName, bool), 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. /// The lockfile uses a different set of version for its workspace members.
MismatchedVersion(PackageName, Version, Option<Version>), MismatchedVersion(PackageName, Version, Option<Version>),
/// The lockfile uses a different set of requirements. /// 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. /// A package in the lockfile contains different `requires-dist` metadata than expected.
MismatchedPackageRequirements( MismatchedPackageRequirements(
&'lock PackageName, &'lock PackageName,
&'lock Version, Option<&'lock Version>,
BTreeSet<Requirement>, BTreeSet<Requirement>,
BTreeSet<Requirement>, BTreeSet<Requirement>,
), ),
/// A package in the lockfile contains different `dependency-group` metadata than expected. /// A package in the lockfile contains different `dependency-group` metadata than expected.
MismatchedPackageDependencyGroups( MismatchedPackageDependencyGroups(
&'lock PackageName, &'lock PackageName,
&'lock Version, Option<&'lock Version>,
BTreeMap<GroupName, BTreeSet<Requirement>>, BTreeMap<GroupName, BTreeSet<Requirement>>,
BTreeMap<GroupName, BTreeSet<Requirement>>, BTreeMap<GroupName, BTreeSet<Requirement>>,
), ),
@ -1953,7 +1942,7 @@ impl Package {
let install_path = absolute_path(workspace_root, path)?; let install_path = absolute_path(workspace_root, path)?;
let path_dist = PathSourceDist { let path_dist = PathSourceDist {
name: self.id.name.clone(), name: self.id.name.clone(),
version: Some(self.id.version.clone()), version: self.id.version.clone(),
url: verbatim_url(&install_path, &self.id)?, url: verbatim_url(&install_path, &self.id)?,
install_path, install_path,
ext, ext,
@ -2047,9 +2036,16 @@ impl Package {
return Ok(None); 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 { let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
name: self.id.name.clone(), name: name.clone(),
version: self.id.version.clone(), version: version.clone(),
})?; })?;
let filename = sdist let filename = sdist
.filename() .filename()
@ -2076,8 +2072,8 @@ impl Package {
)); ));
let reg_dist = RegistrySourceDist { let reg_dist = RegistrySourceDist {
name: self.id.name.clone(), name: name.clone(),
version: self.id.version.clone(), version: version.clone(),
file, file,
ext, ext,
index, index,
@ -2090,9 +2086,16 @@ impl Package {
return Ok(None); 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 { let file_path = sdist.path().ok_or_else(|| LockErrorKind::MissingPath {
name: self.id.name.clone(), name: name.clone(),
version: self.id.version.clone(), version: version.clone(),
})?; })?;
let file_url = Url::from_file_path(workspace_root.join(path).join(file_path)) let file_url = Url::from_file_path(workspace_root.join(path).join(file_path))
.map_err(|()| LockErrorKind::PathToUrl)?; .map_err(|()| LockErrorKind::PathToUrl)?;
@ -2121,8 +2124,8 @@ impl Package {
); );
let reg_dist = RegistrySourceDist { let reg_dist = RegistrySourceDist {
name: self.id.name.clone(), name: name.clone(),
version: self.id.version.clone(), version: version.clone(),
file, file,
ext, ext,
index, index,
@ -2302,8 +2305,8 @@ impl Package {
} }
/// Returns the [`Version`] of the package. /// Returns the [`Version`] of the package.
pub fn version(&self) -> &Version { pub fn version(&self) -> Option<&Version> {
&self.id.version self.id.version.as_ref()
} }
/// Return the fork markers for this package, if any. /// Return the fork markers for this package, if any.
@ -2358,6 +2361,11 @@ impl Package {
_ => Ok(None), _ => 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`. /// 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)] #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
pub(crate) struct PackageId { pub(crate) struct PackageId {
pub(crate) name: PackageName, pub(crate) name: PackageName,
pub(crate) version: Version, pub(crate) version: Option<Version>,
source: Source, source: Source,
} }
@ -2461,9 +2469,20 @@ impl PackageId {
annotated_dist: &AnnotatedDist, annotated_dist: &AnnotatedDist,
root: &Path, root: &Path,
) -> Result<PackageId, LockError> { ) -> Result<PackageId, LockError> {
let name = annotated_dist.name.clone(); // Identify the source of the package.
let version = annotated_dist.version.clone();
let source = Source::from_resolved_dist(&annotated_dist.dist, root)?; 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 { Ok(Self {
name, name,
version, version,
@ -2481,7 +2500,9 @@ impl PackageId {
let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied()); let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
table.insert("name", value(self.name.to_string())); table.insert("name", value(self.name.to_string()));
if count.map(|count| count > 1).unwrap_or(true) { 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); self.source.to_toml(table);
} }
} }
@ -2489,7 +2510,11 @@ impl PackageId {
impl Display for PackageId { impl Display for PackageId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 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>, unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
) -> Result<PackageId, LockError> { ) -> Result<PackageId, LockError> {
let unambiguous_package_id = unambiguous_package_ids.get(&self.name); 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 { let Some(dist_id) = unambiguous_package_id else {
return Err(LockErrorKind::MissingDependencyVersion { return Err(LockErrorKind::MissingDependencyVersion {
name: self.name.clone(), name: self.name.clone(),
} }
.into()); .into());
}; };
Ok(dist_id.version.clone()) dist_id.version.clone()
})?; };
let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| { let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
let Some(package_id) = unambiguous_package_id else { let Some(package_id) = unambiguous_package_id else {
return Err(LockErrorKind::MissingDependencySource { return Err(LockErrorKind::MissingDependencySource {
@ -2536,7 +2563,7 @@ impl From<PackageId> for PackageIdForDependency {
fn from(id: PackageId) -> PackageIdForDependency { fn from(id: PackageId) -> PackageIdForDependency {
PackageIdForDependency { PackageIdForDependency {
name: id.name, name: id.name,
version: Some(id.version), version: id.version,
source: Some(id.source), 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) { fn to_toml(&self, table: &mut Table) {
let mut source_table = InlineTable::new(); let mut source_table = InlineTable::new();
match *self { match *self {
@ -3824,21 +3859,22 @@ impl Dependency {
impl Display for Dependency { impl Display for Dependency {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.extra.is_empty() { match (self.extra.is_empty(), self.package_id.version.as_ref()) {
write!( (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
(true, None) => write!(f, "{}", self.package_id.name),
(false, Some(version)) => write!(
f, f,
"{}=={} @ {}", "{}[{}]=={}",
self.package_id.name, self.package_id.version, self.package_id.source
)
} else {
write!(
f,
"{}[{}]=={} @ {}",
self.package_id.name, self.package_id.name,
self.extra.iter().join(","), self.extra.iter().join(","),
self.package_id.version, version
self.package_id.source ),
) (false, None) => write!(
f,
"{}[{}]",
self.package_id.name,
self.extra.iter().join(",")
),
} }
} }
} }

View file

@ -298,7 +298,12 @@ impl std::fmt::Display for RequirementsTxtExport<'_> {
for Requirement { package, marker } in &self.nodes { for Requirement { package, marker } in &self.nodes {
match &package.id.source { match &package.id.source {
Source::Registry(_) => { 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) => { Source::Git(url, git) => {
// Remove the fragment and query from the URL; they're already present in the // Remove the fragment and query from the URL; they're already present in the

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -94,7 +96,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -101,7 +103,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Path( source: Path(
"file:///foo/bar", "file:///foo/bar",
), ),
@ -97,7 +99,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Path( source: Path(
"file:///foo/bar", "file:///foo/bar",
), ),

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"a", "a",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -86,7 +88,9 @@ Ok(
name: PackageName( name: PackageName(
"b", "b",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -123,7 +127,9 @@ Ok(
name: PackageName( name: PackageName(
"a", "a",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -148,24 +154,13 @@ Ok(
}, },
], ],
by_id: { by_id: {
PackageId {
name: PackageName(
"a",
),
version: "0.1.0",
source: Registry(
Url(
UrlString(
"https://pypi.org/simple",
),
),
),
}: 0,
PackageId { PackageId {
name: PackageName( name: PackageName(
"b", "b",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -174,6 +169,21 @@ Ok(
), ),
), ),
}: 1, }: 1,
PackageId {
name: PackageName(
"a",
),
version: Some(
"0.1.0",
),
source: Registry(
Url(
UrlString(
"https://pypi.org/simple",
),
),
),
}: 0,
}, },
manifest: ResolverManifest { manifest: ResolverManifest {
members: {}, members: {},

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"a", "a",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -86,7 +88,9 @@ Ok(
name: PackageName( name: PackageName(
"b", "b",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -123,7 +127,9 @@ Ok(
name: PackageName( name: PackageName(
"a", "a",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -148,24 +154,13 @@ Ok(
}, },
], ],
by_id: { by_id: {
PackageId {
name: PackageName(
"a",
),
version: "0.1.0",
source: Registry(
Url(
UrlString(
"https://pypi.org/simple",
),
),
),
}: 0,
PackageId { PackageId {
name: PackageName( name: PackageName(
"b", "b",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -174,6 +169,21 @@ Ok(
), ),
), ),
}: 1, }: 1,
PackageId {
name: PackageName(
"a",
),
version: Some(
"0.1.0",
),
source: Registry(
Url(
UrlString(
"https://pypi.org/simple",
),
),
),
}: 0,
}, },
manifest: ResolverManifest { manifest: ResolverManifest {
members: {}, members: {},

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"a", "a",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -86,7 +88,9 @@ Ok(
name: PackageName( name: PackageName(
"b", "b",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -123,7 +127,9 @@ Ok(
name: PackageName( name: PackageName(
"a", "a",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -148,24 +154,13 @@ Ok(
}, },
], ],
by_id: { by_id: {
PackageId {
name: PackageName(
"a",
),
version: "0.1.0",
source: Registry(
Url(
UrlString(
"https://pypi.org/simple",
),
),
),
}: 0,
PackageId { PackageId {
name: PackageName( name: PackageName(
"b", "b",
), ),
version: "0.1.0", version: Some(
"0.1.0",
),
source: Registry( source: Registry(
Url( Url(
UrlString( UrlString(
@ -174,6 +169,21 @@ Ok(
), ),
), ),
}: 1, }: 1,
PackageId {
name: PackageName(
"a",
),
version: Some(
"0.1.0",
),
source: Registry(
Url(
UrlString(
"https://pypi.org/simple",
),
),
),
}: 0,
}, },
manifest: ResolverManifest { manifest: ResolverManifest {
members: {}, members: {},

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Direct( source: Direct(
UrlString( UrlString(
"https://burntsushi.net", "https://burntsushi.net",
@ -71,7 +73,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Direct( source: Direct(
UrlString( UrlString(
"https://burntsushi.net", "https://burntsushi.net",

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Direct( source: Direct(
UrlString( UrlString(
"https://burntsushi.net", "https://burntsushi.net",
@ -69,7 +71,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Direct( source: Direct(
UrlString( UrlString(
"https://burntsushi.net", "https://burntsushi.net",

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Directory( source: Directory(
"path/to/dir", "path/to/dir",
), ),
@ -64,7 +66,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Directory( source: Directory(
"path/to/dir", "path/to/dir",
), ),

View file

@ -42,7 +42,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Editable( source: Editable(
"path/to/dir", "path/to/dir",
), ),
@ -64,7 +66,9 @@ Ok(
name: PackageName( name: PackageName(
"anyio", "anyio",
), ),
version: "4.3.0", version: Some(
"4.3.0",
),
source: Editable( source: Editable(
"path/to/dir", "path/to/dir",
), ),

View file

@ -369,9 +369,11 @@ impl<'env> TreeDisplay<'env> {
} }
} }
line.push(' '); if let Some(version) = package_id.version.as_ref() {
line.push('v'); line.push(' ');
line.push_str(&format!("{}", package_id.version)); line.push('v');
line.push_str(&format!("{version}"));
}
if let Some(edge) = edge { if let Some(edge) = edge {
match edge { match edge {

View file

@ -95,15 +95,18 @@ impl Preference {
pub fn from_lock( pub fn from_lock(
package: &crate::lock::Package, package: &crate::lock::Package,
install_path: &Path, install_path: &Path,
) -> Result<Self, LockError> { ) -> Result<Option<Self>, LockError> {
Ok(Self { let Some(version) = package.version() else {
return Ok(None);
};
Ok(Some(Self {
name: package.id.name.clone(), name: package.id.name.clone(),
version: package.id.version.clone(), version: version.clone(),
marker: MarkerTree::TRUE, marker: MarkerTree::TRUE,
index: package.index(install_path)?, index: package.index(install_path)?,
fork_markers: package.fork_markers().to_vec(), fork_markers: package.fork_markers().to_vec(),
hashes: Vec::new(), hashes: Vec::new(),
}) }))
} }
/// Return the [`PackageName`] of the package for this [`Preference`]. /// Return the [`PackageName`] of the package for this [`Preference`].

View file

@ -87,6 +87,13 @@ impl PyProjectToml {
self.build_system.is_some() 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. /// Returns whether the project manifest contains any script table.
pub fn has_scripts(&self) -> bool { pub fn has_scripts(&self) -> bool {
if let Some(ref project) = self.project { if let Some(ref project) = self.project {

View file

@ -690,7 +690,9 @@ async fn lock_and_sync(
FxHashMap::with_capacity_and_hasher(lock.packages().len(), FxBuildHasher); FxHashMap::with_capacity_and_hasher(lock.packages().len(), FxBuildHasher);
for dist in lock.packages() { for dist in lock.packages() {
let name = dist.name(); let name = dist.name();
let version = dist.version(); let Some(version) = dist.version() else {
continue;
};
match minimum_version.entry(name) { match minimum_version.entry(name) {
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
entry.insert(version); entry.insert(version);

View file

@ -930,7 +930,7 @@ impl ValidatedLock {
); );
Ok(Self::Preferable(lock)) Ok(Self::Preferable(lock))
} }
SatisfiesResult::MismatchedSources(name, expected) => { SatisfiesResult::MismatchedVirtual(name, expected) => {
if expected { if expected {
debug!( debug!(
"Ignoring existing lockfile due to mismatched source: `{name}` (expected: `virtual`)" "Ignoring existing lockfile due to mismatched source: `{name}` (expected: `virtual`)"
@ -942,6 +942,18 @@ impl ValidatedLock {
} }
Ok(Self::Preferable(lock)) 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) => { SatisfiesResult::MismatchedVersion(name, expected, actual) => {
if let Some(actual) = actual { if let Some(actual) = actual {
debug!( debug!(
@ -1006,17 +1018,31 @@ impl ValidatedLock {
Ok(Self::Preferable(lock)) Ok(Self::Preferable(lock))
} }
SatisfiesResult::MismatchedPackageRequirements(name, version, expected, actual) => { SatisfiesResult::MismatchedPackageRequirements(name, version, expected, actual) => {
debug!( if let Some(version) = version {
"Ignoring existing lockfile due to mismatched `requires-dist` for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}", debug!(
expected, actual "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)) Ok(Self::Preferable(lock))
} }
SatisfiesResult::MismatchedPackageDependencyGroups(name, version, expected, actual) => { SatisfiesResult::MismatchedPackageDependencyGroups(name, version, expected, actual) => {
debug!( if let Some(version) = version {
"Ignoring existing lockfile due to mismatched dev dependencies for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}", debug!(
expected, actual "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)) Ok(Self::Preferable(lock))
} }
} }
@ -1048,9 +1074,9 @@ fn report_upgrades(
existing_lock.packages().iter().fold( existing_lock.packages().iter().fold(
FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher), FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher),
|mut acc, package| { |mut acc, package| {
acc.entry(package.name()) if let Some(version) = package.version() {
.or_default() acc.entry(package.name()).or_default().insert(version);
.insert(package.version()); }
acc acc
}, },
) )
@ -1062,9 +1088,9 @@ fn report_upgrades(
new_lock.packages().iter().fold( new_lock.packages().iter().fold(
FxHashMap::with_capacity_and_hasher(new_lock.packages().len(), FxBuildHasher), FxHashMap::with_capacity_and_hasher(new_lock.packages().len(), FxBuildHasher),
|mut acc, package| { |mut acc, package| {
acc.entry(package.name()) if let Some(version) = package.version() {
.or_default() acc.entry(package.name()).or_default().insert(version);
.insert(package.version()); }
acc acc
}, },
); );

View file

@ -257,7 +257,7 @@ pub(crate) async fn tree(
continue; continue;
}; };
reporter.on_fetch_version(package.name(), &version); 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); map.insert(package.clone(), version);
} }
} }

View file

@ -14856,7 +14856,7 @@ fn lock_explicit_default_index() -> Result<()> {
DEBUG Using request timeout of [TIME] DEBUG Using request timeout of [TIME]
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/ DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
DEBUG No workspace root found, using project root 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 }} 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 }} 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] DEBUG Solving with installed Python version: 3.12.[X]
@ -19821,7 +19821,7 @@ fn lock_transitive_git() -> Result<()> {
Ok(()) Ok(())
} }
/// Lock a package that's excluded from the parent workspace, but depends on that parent. /// Lock a package with a dynamic version.
#[test] #[test]
fn lock_dynamic_version() -> Result<()> { fn lock_dynamic_version() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
@ -19883,7 +19883,6 @@ fn lock_dynamic_version() -> Result<()> {
[[package]] [[package]]
name = "project" name = "project"
version = "0.1.0"
source = { editable = "." } source = { editable = "." }
"### "###
); );
@ -19897,26 +19896,14 @@ fn lock_dynamic_version() -> Result<()> {
.child("__init__.py") .child("__init__.py")
.write_str("__version__ = '0.1.1'")?; .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###" 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 success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 1 package in [TIME] 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(); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
@ -19934,7 +19921,6 @@ fn lock_dynamic_version() -> Result<()> {
[[package]] [[package]]
name = "project" name = "project"
version = "0.1.1"
source = { editable = "." } source = { editable = "." }
"### "###
); );
@ -19943,6 +19929,368 @@ fn lock_dynamic_version() -> Result<()> {
Ok(()) 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] #[test]
fn lock_derivation_chain_prod() -> Result<()> { fn lock_derivation_chain_prod() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");

View file

@ -179,7 +179,6 @@ wheels = [
[[package]] [[package]]
name = "black" name = "black"
version = "24.8.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },