Skip to content

Commit 2881ddd

Browse files
committed
uv-resolver: filter out dependencies whose markers are always false
Resolution itself won't produce dependencies that can never be installed. But after we get a resolution, we "simplify" the markers for each dependency when in universal mode relative to the requires-python specification. And this step can produce a marker that always evaluates to false. It doesn't make sense to write these out, so this commit filters them out. We do the filtering in a somewhat principled way. Namely, the `SimplifiedMarkerTree` constructor is now fallible, which forces callers to deal with the fact that dependencies might be excluded after simplification.
1 parent bac074d commit 2881ddd

File tree

5 files changed

+83
-42
lines changed

5 files changed

+83
-42
lines changed

crates/uv-resolver/src/lock/mod.rs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,9 @@ impl Lock {
719719
let fork_markers = each_element_on_its_line_array(
720720
self.fork_markers
721721
.iter()
722-
.map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker.clone()))
722+
.filter_map(|marker| {
723+
SimplifiedMarkerTree::new(&self.requires_python, marker.clone())
724+
})
723725
.filter_map(|marker| marker.try_to_string()),
724726
);
725727
doc.insert("resolution-markers", value(fork_markers));
@@ -729,7 +731,9 @@ impl Lock {
729731
let supported_environments = each_element_on_its_line_array(
730732
self.supported_environments
731733
.iter()
732-
.map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker.clone()))
734+
.filter_map(|marker| {
735+
SimplifiedMarkerTree::new(&self.requires_python, marker.clone())
736+
})
733737
.filter_map(|marker| marker.try_to_string()),
734738
);
735739
doc.insert("supported-markers", value(supported_environments));
@@ -1614,15 +1618,22 @@ impl Package {
16141618
}
16151619

16161620
/// Add the [`AnnotatedDist`] as a dependency of the [`Package`].
1621+
///
1622+
/// This may skip adding the dependency if the combination of its markers
1623+
/// and the requires-python constraint result in a scenario where the
1624+
/// dependency can never be installed.
16171625
fn add_dependency(
16181626
&mut self,
16191627
requires_python: &RequiresPython,
16201628
annotated_dist: &AnnotatedDist,
16211629
marker: MarkerTree,
16221630
root: &Path,
16231631
) -> Result<(), LockError> {
1624-
let new_dep =
1625-
Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
1632+
let Some(new_dep) =
1633+
Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?
1634+
else {
1635+
return Ok(());
1636+
};
16261637
for existing_dep in &mut self.dependencies {
16271638
if existing_dep.package_id == new_dep.package_id
16281639
// It's important that we do a comparison on
@@ -1659,6 +1670,10 @@ impl Package {
16591670
}
16601671

16611672
/// Add the [`AnnotatedDist`] as an optional dependency of the [`Package`].
1673+
///
1674+
/// This may skip adding the dependency if the combination of its markers
1675+
/// and the requires-python constraint result in a scenario where the
1676+
/// dependency can never be installed.
16621677
fn add_optional_dependency(
16631678
&mut self,
16641679
requires_python: &RequiresPython,
@@ -1667,7 +1682,11 @@ impl Package {
16671682
marker: MarkerTree,
16681683
root: &Path,
16691684
) -> Result<(), LockError> {
1670-
let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
1685+
let Some(dep) =
1686+
Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?
1687+
else {
1688+
return Ok(());
1689+
};
16711690
let optional_deps = self.optional_dependencies.entry(extra).or_default();
16721691
for existing_dep in &mut *optional_deps {
16731692
if existing_dep.package_id == dep.package_id
@@ -1685,6 +1704,10 @@ impl Package {
16851704
}
16861705

16871706
/// Add the [`AnnotatedDist`] to a dependency group of the [`Package`].
1707+
///
1708+
/// This may skip adding the dependency if the combination of its markers
1709+
/// and the requires-python constraint result in a scenario where the
1710+
/// dependency can never be installed.
16881711
fn add_group_dependency(
16891712
&mut self,
16901713
requires_python: &RequiresPython,
@@ -1693,7 +1716,11 @@ impl Package {
16931716
marker: MarkerTree,
16941717
root: &Path,
16951718
) -> Result<(), LockError> {
1696-
let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
1719+
let Some(dep) =
1720+
Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?
1721+
else {
1722+
return Ok(());
1723+
};
16971724
let deps = self.dependency_groups.entry(group).or_default();
16981725
for existing_dep in &mut *deps {
16991726
if existing_dep.package_id == dep.package_id
@@ -2026,7 +2053,7 @@ impl Package {
20262053
let wheels = each_element_on_its_line_array(
20272054
self.fork_markers
20282055
.iter()
2029-
.map(|marker| SimplifiedMarkerTree::new(requires_python, marker.clone()))
2056+
.filter_map(|marker| SimplifiedMarkerTree::new(requires_python, marker.clone()))
20302057
.filter_map(|marker| marker.try_to_string()),
20312058
);
20322059
table.insert("resolution-markers", value(wheels));
@@ -3559,23 +3586,27 @@ impl Dependency {
35593586
package_id: PackageId,
35603587
extra: BTreeSet<ExtraName>,
35613588
complexified_marker: MarkerTree,
3562-
) -> Dependency {
3589+
) -> Option<Dependency> {
35633590
let simplified_marker =
3564-
SimplifiedMarkerTree::new(requires_python, complexified_marker.clone());
3565-
Dependency {
3591+
SimplifiedMarkerTree::new(requires_python, complexified_marker.clone())?;
3592+
Some(Dependency {
35663593
package_id,
35673594
extra,
35683595
simplified_marker,
35693596
complexified_marker,
3570-
}
3597+
})
35713598
}
35723599

3600+
/// Creates a new `Dependency` from the given annotated distribution.
3601+
///
3602+
/// This can return `None` if the marker given is simplified by
3603+
/// `requires-python` to a marker that is always false.
35733604
fn from_annotated_dist(
35743605
requires_python: &RequiresPython,
35753606
annotated_dist: &AnnotatedDist,
35763607
complexified_marker: MarkerTree,
35773608
root: &Path,
3578-
) -> Result<Dependency, LockError> {
3609+
) -> Result<Option<Dependency>, LockError> {
35793610
let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
35803611
let extra = annotated_dist.extra.iter().cloned().collect();
35813612
Ok(Dependency::new(

crates/uv-resolver/src/requires_python.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,12 @@ impl RequiresPython {
328328
/// a lock file are deserialized and turned into a `ResolutionGraph`, the
329329
/// markers are "complexified" to put the `requires-python` assumption back
330330
/// into the marker explicitly.
331+
///
332+
/// Note that this may convert a marker tree that is sometime true to
333+
/// a marker tree that is always false. For example, given the marker
334+
/// `python_version < '3.5'` and `requires-python = ">=3.8"`, the marker
335+
/// can never be true. When printed, the marker looks like `python_version
336+
/// < '0'`.
331337
pub(crate) fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree {
332338
let (lower, upper) = (self.range().lower(), self.range().upper());
333339
marker.simplify_python_versions(lower.0.as_ref(), upper.0.as_ref())
@@ -548,11 +554,20 @@ pub(crate) struct SimplifiedMarkerTree(MarkerTree);
548554
impl SimplifiedMarkerTree {
549555
/// Simplifies the given markers by assuming the given `requires-python`
550556
/// bound is true.
557+
///
558+
/// If simplifying the marker expression given would result in a marker
559+
/// that is always false (i.e., the corresponding dependency should never
560+
/// be installed under any circumstances), then this returns `None`.
551561
pub(crate) fn new(
552562
requires_python: &RequiresPython,
553563
marker: MarkerTree,
554-
) -> SimplifiedMarkerTree {
555-
SimplifiedMarkerTree(requires_python.simplify_markers(marker))
564+
) -> Option<SimplifiedMarkerTree> {
565+
let simplified = requires_python.simplify_markers(marker);
566+
if simplified.is_false() {
567+
None
568+
} else {
569+
Some(SimplifiedMarkerTree(simplified))
570+
}
556571
}
557572

558573
/// Complexifies the given markers by adding the given `requires-python` as

crates/uv-resolver/src/resolution/display.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::borrow::Cow;
12
use std::collections::BTreeSet;
23

34
use owo_colors::OwoColorize;
@@ -180,13 +181,16 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
180181
// Print out the dependency graph.
181182
for (index, node) in nodes {
182183
// Display the node itself.
183-
let mut line = node
184+
let Some(mut line) = node
184185
.to_requirements_txt(
185186
&self.resolution.requires_python,
186187
self.include_extras,
187188
self.include_markers,
188189
)
189-
.to_string();
190+
.map(Cow::into_owned)
191+
else {
192+
continue;
193+
};
190194

191195
// Display the distribution hashes, if any.
192196
let mut has_hashes = false;

crates/uv-resolver/src/resolution/requirements_txt.rs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ impl<'dist> RequirementsTxtDist<'dist> {
3737
requires_python: &RequiresPython,
3838
include_extras: bool,
3939
include_markers: bool,
40-
) -> Cow<str> {
40+
) -> Option<Cow<str>> {
4141
// If the URL is editable, write it as an editable requirement.
4242
if self.dist.is_editable() {
4343
if let VersionOrUrlRef::Url(url) = self.dist.version_or_url() {
4444
let given = url.verbatim();
45-
return Cow::Owned(format!("-e {given}"));
45+
return Some(Cow::Owned(format!("-e {given}")));
4646
}
4747
}
4848

@@ -93,21 +93,23 @@ impl<'dist> RequirementsTxtDist<'dist> {
9393
}
9494
};
9595
if let Some(given) = given {
96-
return if let Some(markers) =
97-
SimplifiedMarkerTree::new(requires_python, self.markers.clone())
98-
.try_to_string()
99-
.filter(|_| include_markers)
100-
{
101-
Cow::Owned(format!("{given} ; {markers}"))
102-
} else {
103-
given
104-
};
96+
return Some(
97+
if let Some(markers) =
98+
SimplifiedMarkerTree::new(requires_python, self.markers.clone())?
99+
.try_to_string()
100+
.filter(|_| include_markers)
101+
{
102+
Cow::Owned(format!("{given} ; {markers}"))
103+
} else {
104+
given
105+
},
106+
);
105107
}
106108
}
107109
}
108110

109-
if self.extras.is_empty() || !include_extras {
110-
if let Some(markers) = SimplifiedMarkerTree::new(requires_python, self.markers.clone())
111+
Some(if self.extras.is_empty() || !include_extras {
112+
if let Some(markers) = SimplifiedMarkerTree::new(requires_python, self.markers.clone())?
111113
.try_to_string()
112114
.filter(|_| include_markers)
113115
{
@@ -119,7 +121,7 @@ impl<'dist> RequirementsTxtDist<'dist> {
119121
let mut extras = self.extras.clone();
120122
extras.sort_unstable();
121123
extras.dedup();
122-
if let Some(markers) = SimplifiedMarkerTree::new(requires_python, self.markers.clone())
124+
if let Some(markers) = SimplifiedMarkerTree::new(requires_python, self.markers.clone())?
123125
.try_to_string()
124126
.filter(|_| include_markers)
125127
{
@@ -138,7 +140,7 @@ impl<'dist> RequirementsTxtDist<'dist> {
138140
self.version_or_url().verbatim()
139141
))
140142
}
141-
}
143+
})
142144
}
143145

144146
pub(crate) fn to_comparator(&self) -> RequirementsTxtComparator {

crates/uv/tests/it/pip_compile.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -484,14 +484,10 @@ typing_extensions==4.3.0
484484
apache-airflow==2.3.4 # via apache-airflow-providers-microsoft-azure, -r requirements.in
485485
apache-airflow-providers-common-sql==1.4.0 # via apache-airflow-providers-sqlite
486486
apache-airflow-providers-ftp==3.3.1 # via apache-airflow
487-
apache-airflow-providers-ftp==3.7.0 ; python_version < '0' # via apache-airflow
488487
apache-airflow-providers-http==4.3.0 # via apache-airflow
489-
apache-airflow-providers-http==4.10.0 ; python_version < '0' # via apache-airflow
490488
apache-airflow-providers-imap==3.1.1 # via apache-airflow
491-
apache-airflow-providers-imap==3.5.0 ; python_version < '0' # via apache-airflow
492489
apache-airflow-providers-microsoft-azure==4.2.0 # via apache-airflow, -c constraints.txt
493490
apache-airflow-providers-sqlite==3.3.2 # via apache-airflow
494-
apache-airflow-providers-sqlite==3.7.1 ; python_version < '0' # via apache-airflow
495491
apispec==3.3.2 # via flask-appbuilder
496492
argcomplete==3.2.3 # via apache-airflow
497493
asgiref==3.8.1 # via apache-airflow-providers-http, connexion, flask
@@ -501,13 +497,10 @@ typing_extensions==4.3.0
501497
azure-batch==14.1.0 # via apache-airflow-providers-microsoft-azure
502498
azure-common==1.1.28 # via azure-batch, azure-keyvault-secrets, azure-mgmt-containerinstance, azure-mgmt-datafactory, azure-mgmt-datalake-store, azure-mgmt-resource, azure-storage-common, azure-storage-file
503499
azure-core==1.29.1 # via azure-cosmos, azure-identity, azure-keyvault-secrets, azure-mgmt-core, azure-servicebus, azure-storage-blob, msrest
504-
azure-core==1.30.1 ; python_version < '0' # via azure-identity
505500
azure-cosmos==4.5.1 # via apache-airflow-providers-microsoft-azure
506-
azure-cosmos==4.6.0 ; python_version < '0' # via apache-airflow-providers-microsoft-azure
507501
azure-datalake-store==0.0.53 # via apache-airflow-providers-microsoft-azure
508502
azure-identity==1.10.0 # via apache-airflow-providers-microsoft-azure, -c constraints.txt
509503
azure-keyvault-secrets==4.7.0 # via apache-airflow-providers-microsoft-azure
510-
azure-keyvault-secrets==4.8.0 ; python_version < '0' # via apache-airflow-providers-microsoft-azure
511504
azure-kusto-data==0.0.45 # via apache-airflow-providers-microsoft-azure
512505
azure-mgmt-containerinstance==1.5.0 # via apache-airflow-providers-microsoft-azure
513506
azure-mgmt-core==1.4.0 # via azure-mgmt-datafactory, azure-mgmt-resource
@@ -523,11 +516,9 @@ typing_extensions==4.3.0
523516
azure-storage-file==2.1.0 # via apache-airflow-providers-microsoft-azure
524517
babel==2.14.0 # via flask-babel
525518
black==22.12.0 # via -r requirements.in
526-
black==24.3.0 ; python_version < '0' # via -r requirements.in
527519
blinker==1.7.0 # via apache-airflow
528520
cachelib==0.9.0 # via flask-caching
529521
cattrs==23.1.2 # via apache-airflow
530-
cattrs==23.2.3 ; python_version < '0' # via apache-airflow
531522
certifi==2024.2.2 # via httpcore, httpx, msrest, requests
532523
cffi==1.16.0 # via azure-datalake-store, cryptography
533524
charset-normalizer==3.3.2 # via requests
@@ -571,7 +562,6 @@ typing_extensions==4.3.0
571562
jinja2==3.1.3 # via apache-airflow, connexion, flask, flask-babel, python-nvd3, swagger-ui-bundle
572563
joblib==1.3.2 # via scikit-learn
573564
jsonschema==4.17.3 # via apache-airflow, connexion, flask-appbuilder
574-
jsonschema==4.21.1 ; python_version < '0' # via apache-airflow, flask-appbuilder
575565
lazy-object-proxy==1.10.0 # via apache-airflow
576566
linkify-it-py==2.0.3 # via apache-airflow
577567
lockfile==0.12.2 # via apache-airflow, python-daemon
@@ -626,7 +616,6 @@ typing_extensions==4.3.0
626616
requests-oauthlib==2.0.0 # via msrest
627617
requests-toolbelt==1.0.0 # via apache-airflow-providers-http
628618
rich==13.3.1 # via apache-airflow
629-
rich==13.7.1 ; python_version < '0' # via apache-airflow
630619
scikit-learn==1.2.2 # via -r requirements.in
631620
scipy==1.12.0 # via scikit-learn
632621
setproctitle==1.3.3 # via apache-airflow

0 commit comments

Comments
 (0)