Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 62 additions & 21 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,9 @@ impl Lock {
Some(
FlatRequiresDist::from_requirements(requires_dist.clone(), &package.id.name)
.into_iter()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| {
normalize_requirement(requirement, root, &self.requires_python)
})
.collect::<Result<BTreeSet<_>, _>>()?,
)
} else {
Expand All @@ -1153,14 +1155,14 @@ impl Lock {

// Validate the `requires-dist` metadata.
let expected: BTreeSet<_> = Box::into_iter(requires_dist)
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
let actual: BTreeSet<_> = package
.metadata
.requires_dist
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;

if expected != actual && flattened.is_none_or(|expected| expected != actual) {
Expand All @@ -1180,7 +1182,9 @@ impl Lock {
Ok::<_, LockError>((
group,
Box::into_iter(requirements)
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| {
normalize_requirement(requirement, root, &self.requires_python)
})
.collect::<Result<_, _>>()?,
))
})
Expand All @@ -1196,7 +1200,9 @@ impl Lock {
requirements
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| {
normalize_requirement(requirement, root, &self.requires_python)
})
.collect::<Result<_, _>>()?,
))
})
Expand Down Expand Up @@ -1263,14 +1269,14 @@ impl Lock {
let expected: BTreeSet<_> = requirements
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
let actual: BTreeSet<_> = self
.manifest
.requirements
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
if expected != actual {
return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
Expand All @@ -1282,14 +1288,14 @@ impl Lock {
let expected: BTreeSet<_> = constraints
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
let actual: BTreeSet<_> = self
.manifest
.constraints
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
if expected != actual {
return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
Expand All @@ -1301,14 +1307,14 @@ impl Lock {
let expected: BTreeSet<_> = overrides
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
let actual: BTreeSet<_> = self
.manifest
.overrides
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
if expected != actual {
return Ok(SatisfiesResult::MismatchedOverrides(expected, actual));
Expand All @@ -1320,14 +1326,14 @@ impl Lock {
let expected: BTreeSet<_> = build_constraints
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
let actual: BTreeSet<_> = self
.manifest
.build_constraints
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
.collect::<Result<_, _>>()?;
if expected != actual {
return Ok(SatisfiesResult::MismatchedBuildConstraints(
Expand All @@ -1347,7 +1353,9 @@ impl Lock {
requirements
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| {
normalize_requirement(requirement, root, &self.requires_python)
})
.collect::<Result<_, _>>()?,
))
})
Expand All @@ -1363,7 +1371,9 @@ impl Lock {
requirements
.iter()
.cloned()
.map(|requirement| normalize_requirement(requirement, root))
.map(|requirement| {
normalize_requirement(requirement, root, &self.requires_python)
})
.collect::<Result<_, _>>()?,
))
})
Expand Down Expand Up @@ -2837,6 +2847,34 @@ struct PackageMetadata {
dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
}

impl PackageMetadata {
fn unwire(self, requires_python: &RequiresPython) -> PackageMetadata {
// We need to complexify these markers so things like
// `requires_python < '0'` get normalized to False
let unwire_requirements = |requirements: BTreeSet<Requirement>| -> BTreeSet<Requirement> {
requirements
.into_iter()
.map(|mut requirement| {
let complexified_marker =
requires_python.complexify_markers(requirement.marker);
requirement.marker = complexified_marker;
requirement
})
.collect()
};

PackageMetadata {
requires_dist: unwire_requirements(self.requires_dist),
provides_extras: self.provides_extras,
dependency_groups: self
.dependency_groups
.into_iter()
.map(|(group, requirements)| (group, unwire_requirements(requirements)))
.collect(),
}
}
}

impl PackageWire {
fn unwire(
self,
Expand Down Expand Up @@ -2865,9 +2903,10 @@ impl PackageWire {
.map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
.collect()
};

Ok(Package {
id: self.id,
metadata: self.metadata,
metadata: self.metadata.unwire(requires_python),
sdist: self.sdist,
wheels: self.wheels,
fork_markers: self
Expand Down Expand Up @@ -4546,9 +4585,11 @@ fn normalize_url(mut url: Url) -> UrlString {
/// 2. Ensures that the lock and install paths are appropriately framed with respect to the
/// current [`Workspace`].
/// 3. Removes the `origin` field, which is only used in `requirements.txt`.
/// 4. Simplifies the markers using the provided [`RequiresPython`] instance.
fn normalize_requirement(
mut requirement: Requirement,
root: &Path,
requires_python: &RequiresPython,
) -> Result<Requirement, LockError> {
// Sort the extras and groups for consistency.
requirement.extras.sort();
Expand Down Expand Up @@ -4585,7 +4626,7 @@ fn normalize_requirement(
name: requirement.name,
extras: requirement.extras,
groups: requirement.groups,
marker: requirement.marker,
marker: requires_python.simplify_markers(requirement.marker),
source: RequirementSource::Git {
git,
subdirectory,
Expand All @@ -4608,7 +4649,7 @@ fn normalize_requirement(
name: requirement.name,
extras: requirement.extras,
groups: requirement.groups,
marker: requirement.marker,
marker: requires_python.simplify_markers(requirement.marker),
source: RequirementSource::Path {
install_path,
ext,
Expand All @@ -4632,7 +4673,7 @@ fn normalize_requirement(
name: requirement.name,
extras: requirement.extras,
groups: requirement.groups,
marker: requirement.marker,
marker: requires_python.simplify_markers(requirement.marker),
source: RequirementSource::Directory {
install_path,
editable,
Expand All @@ -4659,7 +4700,7 @@ fn normalize_requirement(
name: requirement.name,
extras: requirement.extras,
groups: requirement.groups,
marker: requirement.marker,
marker: requires_python.simplify_markers(requirement.marker),
source: RequirementSource::Registry {
specifier,
index,
Expand Down Expand Up @@ -4691,7 +4732,7 @@ fn normalize_requirement(
name: requirement.name,
extras: requirement.extras,
groups: requirement.groups,
marker: requirement.marker,
marker: requires_python.simplify_markers(requirement.marker),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused as to whether this should be simplify or complexify. It might not matter as long as it's consistent?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that sounds right to me.

source: RequirementSource::Url {
location,
subdirectory,
Expand Down
83 changes: 83 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11767,6 +11767,89 @@ fn unconditional_overlapping_marker_disjoint_version_constraints() -> Result<()>
Ok(())
}

/// This checks that markers that normalize to 'false', which are serialized
/// to the lockfile as `python_full_version < '0'`, get read back as false.
/// Otherwise `uv lock --check` will always fail.
#[test]
fn normalize_false_marker_dependency_groups() -> 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.11"
[dependency-groups]
dev = [
"pytest;python_full_version>'3.8' and python_full_version<'3.6'"
]
"#,
)?;

uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
");

uv_snapshot!(context.filters(), context.lock().arg("--check"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
");

Ok(())
}

/// This checks that markers that normalize to 'false', which are serialized
/// to the lockfile as `python_full_version < '0'`, get read back as false.
/// Otherwise `uv lock --check` will always fail.
#[test]
fn normalize_false_marker_requires_dist() -> Result<()> {
let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "debug"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"pytest; python_full_version>'3.8' and python_full_version<'3.6'"
]
"#,
)?;

uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
");

uv_snapshot!(context.filters(), context.lock().arg("--check"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
");

Ok(())
}

/// Change indexes between locking operations.
#[test]
fn lock_change_index() -> Result<()> {
Expand Down
Loading