diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index ae8aeb63b3e3..3951bbd85995 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2751,17 +2751,6 @@ impl PackageIdForDependency { unambiguous_package_ids: &FxHashMap, ) -> Result { let unambiguous_package_id = unambiguous_package_ids.get(&self.name); - 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()); - }; - 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 { @@ -2771,6 +2760,24 @@ impl PackageIdForDependency { }; Ok(package_id.source.clone()) })?; + let version = if let Some(version) = self.version { + Some(version) + } else { + if let Some(package_id) = unambiguous_package_id { + package_id.version.clone() + } else { + // If the package is a source tree, assume that the missing `self.version` field is + // indicative of a dynamic version. + if source.is_source_tree() { + None + } else { + return Err(LockErrorKind::MissingDependencyVersion { + name: self.name.clone(), + } + .into()); + } + } + }; Ok(PackageId { name: self.name, version, @@ -5040,13 +5047,13 @@ requires-python = ">=3.12" [[package]] name = "a" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] name = "b" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package.dependencies]] @@ -5066,18 +5073,18 @@ requires-python = ">=3.12" [[package]] name = "a" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] name = "b" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package.dependencies]] name = "a" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } "#; let result: Result = toml::from_str(data); insta::assert_debug_snapshot!(result); @@ -5092,13 +5099,13 @@ requires-python = ">=3.12" [[package]] name = "a" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] name = "b" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package.dependencies]] @@ -5117,19 +5124,19 @@ requires-python = ">=3.12" [[package]] name = "a" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] name = "a" version = "0.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] name = "b" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package.dependencies]] @@ -5149,24 +5156,24 @@ requires-python = ">=3.12" [[package]] name = "a" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] name = "a" version = "0.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] name = "b" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package.dependencies]] name = "a" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } "#; let result = toml::from_str::(data).unwrap_err(); assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package"); @@ -5181,7 +5188,7 @@ requires-python = ">=3.12" [[package]] name = "a" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package]] @@ -5193,14 +5200,44 @@ sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d [[package]] name = "b" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } [[package.dependencies]] name = "a" "#; let result = toml::from_str::(data).unwrap_err(); - assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package"); + assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package"); + } + + #[test] + fn missing_dependency_version_dynamic() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "a" +source = { editable = "path/to/a" } + +[[package]] +name = "a" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "b" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package.dependencies]] +name = "a" +source = { editable = "path/to/a" } +"#; + let result = toml::from_str::(data); + insta::assert_debug_snapshot!(result); } #[test] diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap new file mode 100644 index 000000000000..8a0726607b62 --- /dev/null +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap @@ -0,0 +1,221 @@ +--- +source: crates/uv-resolver/src/lock/mod.rs +expression: result +--- +Ok( + Lock { + version: 1, + fork_markers: [], + conflicts: Conflicts( + [], + ), + supported_environments: [], + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + LowerBound( + Included( + "3.12", + ), + ), + UpperBound( + Unbounded, + ), + ), + }, + options: ResolverOptions { + resolution_mode: Highest, + prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, + exclude_newer: None, + }, + packages: [ + Package { + id: PackageId { + name: PackageName( + "a", + ), + version: None, + source: Editable( + "path/to/a", + ), + }, + sdist: None, + wheels: [], + fork_markers: [], + dependencies: [], + optional_dependencies: {}, + dependency_groups: {}, + metadata: PackageMetadata { + requires_dist: {}, + dependency_groups: {}, + }, + }, + Package { + id: PackageId { + name: PackageName( + "a", + ), + version: Some( + "0.1.1", + ), + source: Registry( + Url( + UrlString( + "https://pypi.org/simple", + ), + ), + ), + }, + sdist: Some( + Url { + url: UrlString( + "https://example.com", + ), + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + fork_markers: [], + dependencies: [], + optional_dependencies: {}, + dependency_groups: {}, + metadata: PackageMetadata { + requires_dist: {}, + dependency_groups: {}, + }, + }, + Package { + id: PackageId { + name: PackageName( + "b", + ), + version: Some( + "0.1.0", + ), + source: Registry( + Url( + UrlString( + "https://pypi.org/simple", + ), + ), + ), + }, + sdist: Some( + Url { + url: UrlString( + "https://example.com", + ), + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + fork_markers: [], + dependencies: [ + Dependency { + package_id: PackageId { + name: PackageName( + "a", + ), + version: None, + source: Editable( + "path/to/a", + ), + }, + extra: {}, + simplified_marker: SimplifiedMarkerTree( + true, + ), + complexified_marker: python_full_version >= '3.12', + }, + ], + optional_dependencies: {}, + dependency_groups: {}, + metadata: PackageMetadata { + requires_dist: {}, + dependency_groups: {}, + }, + }, + ], + by_id: { + PackageId { + name: PackageName( + "a", + ), + version: None, + source: Editable( + "path/to/a", + ), + }: 0, + PackageId { + name: PackageName( + "a", + ), + version: Some( + "0.1.1", + ), + source: Registry( + Url( + UrlString( + "https://pypi.org/simple", + ), + ), + ), + }: 1, + PackageId { + name: PackageName( + "b", + ), + version: Some( + "0.1.0", + ), + source: Registry( + Url( + UrlString( + "https://pypi.org/simple", + ), + ), + ), + }: 2, + }, + manifest: ResolverManifest { + members: {}, + requirements: {}, + dependency_groups: {}, + constraints: {}, + overrides: {}, + dependency_metadata: {}, + }, + }, +) diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index d7f940fd9b51..f8880d747493 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -17220,7 +17220,7 @@ fn lock_unsupported_version() -> Result<()> { ----- stderr ----- error: Failed to parse `uv.lock`, which uses an unsupported schema version (v2, but only v1 is supported). Downgrade to a compatible uv version, or remove the `uv.lock` prior to running `uv lock` or `uv sync`. - Caused by: Dependency `iniconfig` has missing `version` field but has more than one matching package + Caused by: Dependency `iniconfig` has missing `source` field but has more than one matching package "###); Ok(())