mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 02:22:19 +00:00
Always narrow markers by Python version (#6076)
## Summary Using https://github.com/astral-sh/uv/issues/6064 as a motivating example: at present, on main, we're not properly propagating the `Requires-Python` simplifications. In that case, for example, we end up solving for a branch with `python_version < 3.11`, and a branch `>= 3.11`, even though `Requires-Python` is `>=3.11`. Later, when we get to the graph, we apply version simplification based on `Requires-Python`, which causes us to _remove_ the `python_version < 3.11` markers entirely, leaving us with duplicate dependencies for `pylint`. This PR instead tries to ensure that we always apply this narrowing to requirements and forks, so that we don't need to apply the same simplification when constructing the graph at all. Closes https://github.com/astral-sh/uv/issues/6064. Closes #6059.
This commit is contained in:
parent
f988e43ebd
commit
fe0b873352
11 changed files with 699 additions and 584 deletions
|
@ -105,6 +105,19 @@ impl Lock {
|
|||
python_full_version.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
for markers in &mut package.fork_markers {
|
||||
*markers = markers.clone().simplify_python_versions(
|
||||
python_version.clone(),
|
||||
python_full_version.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for markers in &mut lock.fork_markers {
|
||||
*markers = markers
|
||||
.clone()
|
||||
.simplify_python_versions(python_version.clone(), python_full_version.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use pep508_rs::MarkerTree;
|
||||
use uv_python::{Interpreter, PythonVersion};
|
||||
|
||||
use crate::{RequiresPython, RequiresPythonBound};
|
||||
|
@ -70,17 +69,6 @@ impl PythonRequirement {
|
|||
pub fn target(&self) -> Option<&PythonTarget> {
|
||||
self.target.as_ref()
|
||||
}
|
||||
|
||||
/// Return a [`MarkerTree`] representing the Python requirement.
|
||||
///
|
||||
/// See: [`RequiresPython::to_marker_tree`]
|
||||
pub fn to_marker_tree(&self) -> Option<MarkerTree> {
|
||||
if let Some(PythonTarget::RequiresPython(requires_python)) = self.target.as_ref() {
|
||||
Some(requires_python.to_marker_tree())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
|
|
|
@ -6,8 +6,7 @@ use itertools::Itertools;
|
|||
use pubgrub::Range;
|
||||
|
||||
use distribution_filename::WheelFilename;
|
||||
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifiers};
|
||||
use pep508_rs::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RequiresPythonError {
|
||||
|
@ -196,65 +195,6 @@ impl RequiresPython {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns this `Requires-Python` specifier as an equivalent marker
|
||||
/// expression utilizing the `python_version` marker field.
|
||||
///
|
||||
/// This is useful for comparing a `Requires-Python` specifier with
|
||||
/// arbitrary marker expressions. For example, one can ask whether the
|
||||
/// returned marker expression is disjoint with another marker expression.
|
||||
/// If it is, then one can conclude that the `Requires-Python` specifier
|
||||
/// excludes the dependency with that other marker expression.
|
||||
///
|
||||
/// If this `Requires-Python` specifier has no constraints, then this
|
||||
/// returns a marker tree that evaluates to `true` for all possible marker
|
||||
/// environments.
|
||||
pub fn to_marker_tree(&self) -> MarkerTree {
|
||||
let (op, version) = match self.bound.as_ref() {
|
||||
// If we see this anywhere, then it implies the marker
|
||||
// tree we would generate would always evaluate to
|
||||
// `true` because every possible Python version would
|
||||
// satisfy it.
|
||||
Bound::Unbounded => return MarkerTree::TRUE,
|
||||
Bound::Excluded(version) => (Operator::GreaterThan, version.clone().without_local()),
|
||||
Bound::Included(version) => {
|
||||
(Operator::GreaterThanEqual, version.clone().without_local())
|
||||
}
|
||||
};
|
||||
// For the `python_version` marker expression, it specifically only
|
||||
// supports truncate major/minor versions of Python. This means that
|
||||
// a `Requires-Python: 3.10.1` is satisfied by `python_version ==
|
||||
// '3.10'`. So for disjointness checking, we need to ensure that the
|
||||
// marker expression we generate for `Requires-Python` doesn't try to
|
||||
// be overly selective about the patch version. We do this by keeping
|
||||
// this part of our marker limited to the major and minor version
|
||||
// components only.
|
||||
let version_major_minor_only = Version::new(version.release().iter().take(2));
|
||||
let expr_python_version = MarkerExpression::Version {
|
||||
key: MarkerValueVersion::PythonVersion,
|
||||
// OK because a version specifier is only invalid when the
|
||||
// version is local (which is impossible here because we
|
||||
// strip it above) or if the operator is ~= (which is also
|
||||
// impossible here).
|
||||
specifier: VersionSpecifier::from_version(op, version_major_minor_only).unwrap(),
|
||||
};
|
||||
let expr_python_full_version = MarkerExpression::Version {
|
||||
key: MarkerValueVersion::PythonFullVersion,
|
||||
// For `python_full_version`, we can use the entire
|
||||
// version as-is.
|
||||
//
|
||||
// OK because a version specifier is only invalid when the
|
||||
// version is local (which is impossible here because we
|
||||
// strip it above) or if the operator is ~= (which is also
|
||||
// impossible here).
|
||||
specifier: VersionSpecifier::from_version(op, version).unwrap(),
|
||||
};
|
||||
|
||||
let mut conjunction = MarkerTree::TRUE;
|
||||
conjunction.and(MarkerTree::expression(expr_python_version));
|
||||
conjunction.and(MarkerTree::expression(expr_python_full_version));
|
||||
conjunction
|
||||
}
|
||||
|
||||
/// Returns `false` if the wheel's tags state it can't be used in the given Python version
|
||||
/// range.
|
||||
///
|
||||
|
|
|
@ -3,7 +3,6 @@ use petgraph::{
|
|||
graph::{Graph, NodeIndex},
|
||||
Directed, Direction,
|
||||
};
|
||||
use pubgrub::Range;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
|
||||
use distribution_types::{
|
||||
|
@ -169,21 +168,6 @@ impl ResolutionGraph {
|
|||
.and_then(PythonTarget::as_requires_python)
|
||||
.cloned();
|
||||
|
||||
// Normalize any markers.
|
||||
if let Some(ref requires_python) = requires_python {
|
||||
for edge in petgraph.edge_indices() {
|
||||
petgraph[edge] = petgraph[edge].clone().simplify_python_versions(
|
||||
Range::from(requires_python.bound_major_minor().clone()),
|
||||
Range::from(requires_python.bound().clone()),
|
||||
);
|
||||
}
|
||||
// The above simplification may turn some markers into
|
||||
// "always false." In which case, we should remove that
|
||||
// edge since it can never be traversed in any marker
|
||||
// environment.
|
||||
petgraph.retain_edges(|graph, edge| !graph[edge].is_false());
|
||||
}
|
||||
|
||||
let fork_markers = if let [resolution] = resolutions {
|
||||
match resolution.markers {
|
||||
ResolverMarkers::Universal { .. } | ResolverMarkers::SpecificEnvironment(_) => {
|
||||
|
|
|
@ -103,14 +103,6 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
|
|||
markers: ResolverMarkers,
|
||||
python_requirement: PythonRequirement,
|
||||
workspace_members: BTreeSet<PackageName>,
|
||||
/// This is derived from `PythonRequirement` once at initialization
|
||||
/// time. It's used in universal mode to filter our dependencies with
|
||||
/// a `python_version` marker expression that has no overlap with the
|
||||
/// `Requires-Python` specifier.
|
||||
///
|
||||
/// This is non-None if and only if the resolver is operating in
|
||||
/// universal mode. (i.e., when `markers` is `None`.)
|
||||
requires_python: Option<MarkerTree>,
|
||||
selector: CandidateSelector,
|
||||
index: InMemoryIndex,
|
||||
installed_packages: InstalledPackages,
|
||||
|
@ -233,11 +225,6 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
|
|||
preferences: manifest.preferences,
|
||||
exclusions: manifest.exclusions,
|
||||
hasher: hasher.clone(),
|
||||
requires_python: if markers.marker_environment().is_some() {
|
||||
None
|
||||
} else {
|
||||
python_requirement.to_marker_tree()
|
||||
},
|
||||
markers,
|
||||
python_requirement: python_requirement.clone(),
|
||||
installed_packages,
|
||||
|
@ -322,7 +309,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
root,
|
||||
self.markers.clone(),
|
||||
self.python_requirement.clone(),
|
||||
self.requires_python.clone(),
|
||||
);
|
||||
let mut preferences = self.preferences.clone();
|
||||
let mut forked_states =
|
||||
|
@ -343,7 +329,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
|
||||
'FORK: while let Some(mut state) = forked_states.pop() {
|
||||
if let ResolverMarkers::Fork(markers) = &state.markers {
|
||||
if let Some(requires_python) = state.requires_python.as_ref() {
|
||||
if let Some(requires_python) = state
|
||||
.python_requirement
|
||||
.target()
|
||||
.and_then(|target| target.as_requires_python())
|
||||
{
|
||||
debug!(
|
||||
"Solving split {:?} (requires-python: {:?})",
|
||||
markers, requires_python
|
||||
|
@ -540,7 +530,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&version,
|
||||
&state.fork_urls,
|
||||
&state.markers,
|
||||
state.requires_python.as_ref(),
|
||||
&state.python_requirement,
|
||||
)?;
|
||||
match forked_deps {
|
||||
ForkedDependencies::Unavailable(reason) => {
|
||||
|
@ -1172,9 +1162,10 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
version: &Version,
|
||||
fork_urls: &ForkUrls,
|
||||
markers: &ResolverMarkers,
|
||||
requires_python: Option<&MarkerTree>,
|
||||
python_requirement: &PythonRequirement,
|
||||
) -> Result<ForkedDependencies, ResolveError> {
|
||||
let result = self.get_dependencies(package, version, fork_urls, markers, requires_python);
|
||||
let result =
|
||||
self.get_dependencies(package, version, fork_urls, markers, python_requirement);
|
||||
match markers {
|
||||
ResolverMarkers::SpecificEnvironment(_) => result.map(|deps| match deps {
|
||||
Dependencies::Available(deps) | Dependencies::Unforkable(deps) => {
|
||||
|
@ -1182,7 +1173,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
}
|
||||
Dependencies::Unavailable(err) => ForkedDependencies::Unavailable(err),
|
||||
}),
|
||||
ResolverMarkers::Universal { .. } | ResolverMarkers::Fork(_) => Ok(result?.fork()),
|
||||
ResolverMarkers::Universal { .. } | ResolverMarkers::Fork(_) => {
|
||||
Ok(result?.fork(python_requirement))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1194,7 +1187,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
version: &Version,
|
||||
fork_urls: &ForkUrls,
|
||||
markers: &ResolverMarkers,
|
||||
requires_python: Option<&MarkerTree>,
|
||||
python_requirement: &PythonRequirement,
|
||||
) -> Result<Dependencies, ResolveError> {
|
||||
let url = package.name().and_then(|name| fork_urls.get(name));
|
||||
let dependencies = match &**package {
|
||||
|
@ -1207,7 +1200,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
None,
|
||||
None,
|
||||
markers,
|
||||
requires_python,
|
||||
python_requirement,
|
||||
);
|
||||
|
||||
requirements
|
||||
|
@ -1321,7 +1314,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
dev.as_ref(),
|
||||
Some(name),
|
||||
markers,
|
||||
requires_python,
|
||||
python_requirement,
|
||||
);
|
||||
|
||||
let mut dependencies = requirements
|
||||
|
@ -1445,7 +1438,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
dev: Option<&'a GroupName>,
|
||||
name: Option<&PackageName>,
|
||||
markers: &'a ResolverMarkers,
|
||||
requires_python: Option<&'a MarkerTree>,
|
||||
python_requirement: &'a PythonRequirement,
|
||||
) -> Vec<Cow<'a, Requirement>> {
|
||||
// Start with the requirements for the current extra of the package (for an extra
|
||||
// requirement) or the non-extra (regular) dependencies (if extra is None), plus
|
||||
|
@ -1460,7 +1453,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
regular_and_dev_dependencies,
|
||||
extra,
|
||||
markers,
|
||||
requires_python,
|
||||
python_requirement,
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -1485,7 +1478,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
continue;
|
||||
}
|
||||
for requirement in
|
||||
self.requirements_for_extra(dependencies, Some(&extra), markers, requires_python)
|
||||
self.requirements_for_extra(dependencies, Some(&extra), markers, python_requirement)
|
||||
{
|
||||
if name == Some(&requirement.name) {
|
||||
// Add each transitively included extra.
|
||||
|
@ -1510,32 +1503,44 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
dependencies: impl IntoIterator<Item = &'data Requirement> + 'parameters,
|
||||
extra: Option<&'parameters ExtraName>,
|
||||
markers: &'parameters ResolverMarkers,
|
||||
requires_python: Option<&'parameters MarkerTree>,
|
||||
python_requirement: &'parameters PythonRequirement,
|
||||
) -> impl Iterator<Item = Cow<'data, Requirement>> + 'parameters
|
||||
where
|
||||
'data: 'parameters,
|
||||
{
|
||||
self.overrides
|
||||
.apply(dependencies)
|
||||
.filter(move |requirement| {
|
||||
.filter_map(move |requirement| {
|
||||
// If the requirement would not be selected with any Python version
|
||||
// supported by the root, skip it.
|
||||
if !satisfies_requires_python(requires_python, requirement) {
|
||||
trace!(
|
||||
"skipping {requirement} because of Requires-Python {requires_python:?}",
|
||||
// OK because this filter only applies when there is a present
|
||||
// Requires-Python specifier.
|
||||
requires_python = requires_python.unwrap()
|
||||
let requirement = if let Some(requires_python) = python_requirement.target().and_then(|target| target.as_requires_python()).filter(|_| !requirement.marker.is_true()) {
|
||||
let marker = requirement.marker.clone().simplify_python_versions(
|
||||
Range::from(requires_python.bound_major_minor().clone()),
|
||||
Range::from(requires_python.bound().clone()),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if marker.is_false() {
|
||||
debug!("skipping {requirement} because of Requires-Python: {requires_python}");
|
||||
return None;
|
||||
}
|
||||
|
||||
Cow::Owned(Requirement {
|
||||
name: requirement.name.clone(),
|
||||
extras: requirement.extras.clone(),
|
||||
source: requirement.source.clone(),
|
||||
origin: requirement.origin.clone(),
|
||||
marker
|
||||
})
|
||||
} else {
|
||||
requirement
|
||||
};
|
||||
|
||||
// If we're in a fork in universal mode, ignore any dependency that isn't part of
|
||||
// this fork (but will be part of another fork).
|
||||
if let ResolverMarkers::Fork(markers) = markers {
|
||||
if !possible_to_satisfy_markers(markers, requirement) {
|
||||
trace!("skipping {requirement} because of context resolver markers {markers:?}");
|
||||
return false;
|
||||
if markers.is_disjoint(&requirement.marker) {
|
||||
debug!("skipping {requirement} because of context resolver markers {markers:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1544,23 +1549,23 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
Some(source_extra) => {
|
||||
// Only include requirements that are relevant for the current extra.
|
||||
if requirement.evaluate_markers(markers.marker_environment(), &[]) {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
if !requirement.evaluate_markers(
|
||||
markers.marker_environment(),
|
||||
std::slice::from_ref(source_extra),
|
||||
) {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !requirement.evaluate_markers(markers.marker_environment(), &[]) {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
Some(requirement)
|
||||
})
|
||||
.flat_map(move |requirement| {
|
||||
iter::once(requirement.clone()).chain(
|
||||
|
@ -1568,21 +1573,57 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
.get(&requirement.name)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(move |constraint| {
|
||||
if !satisfies_requires_python(requires_python, constraint) {
|
||||
trace!(
|
||||
"skipping {constraint} because of Requires-Python {requires_python:?}",
|
||||
requires_python = requires_python.unwrap()
|
||||
.filter_map(move |constraint| {
|
||||
// If the requirement would not be selected with any Python version
|
||||
// supported by the root, skip it.
|
||||
let constraint = if let Some(requires_python) = python_requirement.target().and_then(|target| target.as_requires_python()).filter(|_| !constraint.marker.is_true()) {
|
||||
let mut marker = constraint.marker.clone().simplify_python_versions(
|
||||
Range::from(requires_python.bound_major_minor().clone()),
|
||||
Range::from(requires_python.bound().clone()),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
marker.and(requirement.marker.clone());
|
||||
|
||||
// Additionally, if the requirement is `requests ; sys_platform == 'darwin'`
|
||||
// and the constraint is `requests ; python_version == '3.6'`, the
|
||||
// constraint should only apply when _both_ markers are true.
|
||||
if marker.is_false() {
|
||||
debug!("skipping {constraint} because of Requires-Python: {requires_python}");
|
||||
return None;
|
||||
}
|
||||
|
||||
Cow::Owned(Requirement {
|
||||
name: constraint.name.clone(),
|
||||
extras: constraint.extras.clone(),
|
||||
source: constraint.source.clone(),
|
||||
origin: constraint.origin.clone(),
|
||||
marker
|
||||
})
|
||||
} else {
|
||||
// Additionally, if the requirement is `requests ; sys_platform == 'darwin'`
|
||||
// and the constraint is `requests ; python_version == '3.6'`, the
|
||||
// constraint should only apply when _both_ markers are true.
|
||||
if requirement.marker.is_true() {
|
||||
Cow::Borrowed(constraint)
|
||||
} else {
|
||||
let mut marker = constraint.marker.clone();
|
||||
marker.and(requirement.marker.clone());
|
||||
|
||||
Cow::Owned(Requirement {
|
||||
name: constraint.name.clone(),
|
||||
extras: constraint.extras.clone(),
|
||||
source: constraint.source.clone(),
|
||||
origin: constraint.origin.clone(),
|
||||
marker
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// If we're in a fork in universal mode, ignore any dependency that isn't part of
|
||||
// this fork (but will be part of another fork).
|
||||
if let ResolverMarkers::Fork(markers) = markers {
|
||||
if !possible_to_satisfy_markers(markers, constraint) {
|
||||
trace!("skipping {constraint} because of context resolver markers {markers:?}");
|
||||
return false;
|
||||
if markers.is_disjoint(&constraint.marker) {
|
||||
debug!("skipping {constraint} because of context resolver markers {markers:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1593,36 +1634,17 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
markers.marker_environment(),
|
||||
std::slice::from_ref(source_extra),
|
||||
) {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !constraint.evaluate_markers(markers.marker_environment(), &[]) {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(move |constraint| {
|
||||
// If the requirement is `requests ; sys_platform == 'darwin'` and the
|
||||
// constraint is `requests ; python_version == '3.6'`, the constraint
|
||||
// should only apply when _both_ markers are true.
|
||||
if requirement.marker.is_true() {
|
||||
Cow::Borrowed(constraint)
|
||||
} else {
|
||||
let mut marker = constraint.marker.clone();
|
||||
marker.and(requirement.marker.clone());
|
||||
|
||||
Cow::Owned(Requirement {
|
||||
name: constraint.name.clone(),
|
||||
extras: constraint.extras.clone(),
|
||||
source: constraint.source.clone(),
|
||||
origin: constraint.origin.clone(),
|
||||
marker
|
||||
})
|
||||
}
|
||||
Some(constraint)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
@ -2074,8 +2096,6 @@ struct ForkState {
|
|||
/// The top fork has a narrower Python compatibility range, and thus can find a
|
||||
/// solution that omits Python 3.8 support.
|
||||
python_requirement: PythonRequirement,
|
||||
/// The [`MarkerTree`] corresponding to the [`PythonRequirement`].
|
||||
requires_python: Option<MarkerTree>,
|
||||
}
|
||||
|
||||
impl ForkState {
|
||||
|
@ -2084,7 +2104,6 @@ impl ForkState {
|
|||
root: PubGrubPackage,
|
||||
markers: ResolverMarkers,
|
||||
python_requirement: PythonRequirement,
|
||||
requires_python: Option<MarkerTree>,
|
||||
) -> Self {
|
||||
Self {
|
||||
pubgrub,
|
||||
|
@ -2095,7 +2114,6 @@ impl ForkState {
|
|||
added_dependencies: FxHashMap::default(),
|
||||
markers,
|
||||
python_requirement,
|
||||
requires_python,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2257,11 +2275,6 @@ impl ForkState {
|
|||
if let Some(target) = python_requirement.target() {
|
||||
debug!("Narrowed `requires-python` bound to: {target}");
|
||||
}
|
||||
self.requires_python = if self.requires_python.is_some() {
|
||||
python_requirement.to_marker_tree()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.python_requirement = python_requirement;
|
||||
}
|
||||
|
||||
|
@ -2617,7 +2630,7 @@ impl Dependencies {
|
|||
/// A fork *only* occurs when there are multiple dependencies with the same
|
||||
/// name *and* those dependency specifications have corresponding marker
|
||||
/// expressions that are completely disjoint with one another.
|
||||
fn fork(self) -> ForkedDependencies {
|
||||
fn fork(self, python_requirement: &PythonRequirement) -> ForkedDependencies {
|
||||
let deps = match self {
|
||||
Dependencies::Available(deps) => deps,
|
||||
Dependencies::Unforkable(deps) => return ForkedDependencies::Unforked(deps),
|
||||
|
@ -2635,7 +2648,7 @@ impl Dependencies {
|
|||
let Forks {
|
||||
mut forks,
|
||||
diverging_packages,
|
||||
} = Forks::new(name_to_deps);
|
||||
} = Forks::new(name_to_deps, python_requirement);
|
||||
if forks.is_empty() {
|
||||
ForkedDependencies::Unforked(vec![])
|
||||
} else if forks.len() == 1 {
|
||||
|
@ -2693,7 +2706,10 @@ struct Forks {
|
|||
}
|
||||
|
||||
impl Forks {
|
||||
fn new(name_to_deps: BTreeMap<PackageName, Vec<PubGrubDependency>>) -> Forks {
|
||||
fn new(
|
||||
name_to_deps: BTreeMap<PackageName, Vec<PubGrubDependency>>,
|
||||
python_requirement: &PythonRequirement,
|
||||
) -> Forks {
|
||||
let mut forks = vec![Fork {
|
||||
dependencies: vec![],
|
||||
markers: MarkerTree::TRUE,
|
||||
|
@ -2722,6 +2738,7 @@ impl Forks {
|
|||
continue;
|
||||
}
|
||||
for dep in deps {
|
||||
// We assume that the marker has already been Python-simplified.
|
||||
let mut markers = dep.package.marker().cloned().unwrap_or(MarkerTree::TRUE);
|
||||
if markers.is_false() {
|
||||
// If the markers can never be satisfied, then we
|
||||
|
@ -2747,9 +2764,10 @@ impl Forks {
|
|||
new.push(fork);
|
||||
continue;
|
||||
}
|
||||
let not_markers = markers.negate();
|
||||
|
||||
let not_markers = simplify_python(markers.negate(), python_requirement);
|
||||
let mut new_markers = markers.clone();
|
||||
new_markers.and(fork.markers.negate());
|
||||
new_markers.and(simplify_python(fork.markers.negate(), python_requirement));
|
||||
if !fork.markers.is_disjoint(¬_markers) {
|
||||
let mut new_fork = fork.clone();
|
||||
new_fork.intersect(not_markers);
|
||||
|
@ -2856,24 +2874,17 @@ impl PartialOrd for Fork {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if the given requirement's marker expression has a
|
||||
/// possible true value given the `requires_python` specifier given.
|
||||
///
|
||||
/// While this is always called, a `requires_python` is only non-None when in
|
||||
/// universal resolution mode. In non-universal mode, `requires_python` is
|
||||
/// `None` and this always returns `true`.
|
||||
fn satisfies_requires_python(
|
||||
requires_python: Option<&MarkerTree>,
|
||||
requirement: &Requirement,
|
||||
) -> bool {
|
||||
let Some(requires_python) = requires_python else {
|
||||
return true;
|
||||
};
|
||||
possible_to_satisfy_markers(requires_python, requirement)
|
||||
}
|
||||
|
||||
/// Returns true if and only if the given requirement's marker expression has a
|
||||
/// possible true value given the `markers` expression given.
|
||||
fn possible_to_satisfy_markers(markers: &MarkerTree, requirement: &Requirement) -> bool {
|
||||
!markers.is_disjoint(&requirement.marker)
|
||||
/// Simplify a [`MarkerTree`] based on a [`PythonRequirement`].
|
||||
fn simplify_python(marker: MarkerTree, python_requirement: &PythonRequirement) -> MarkerTree {
|
||||
if let Some(requires_python) = python_requirement
|
||||
.target()
|
||||
.and_then(|target| target.as_requires_python())
|
||||
{
|
||||
marker.simplify_python_versions(
|
||||
Range::from(requires_python.bound_major_minor().clone()),
|
||||
Range::from(requires_python.bound().clone()),
|
||||
)
|
||||
} else {
|
||||
marker
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ fn branching_urls_overlapping() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Requirements contain conflicting URLs for package `iniconfig` in split `python_version >= '3.11' and python_version < '3.12'`:
|
||||
error: Requirements contain conflicting URLs for package `iniconfig` in split `python_version < '3.12'`:
|
||||
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
||||
- https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl
|
||||
"###
|
||||
|
@ -723,7 +723,7 @@ fn branching_urls_of_different_sources_conflict() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Requirements contain conflicting URLs for package `iniconfig` in split `python_version >= '3.11' and python_version < '3.12'`:
|
||||
error: Requirements contain conflicting URLs for package `iniconfig` in split `python_version < '3.12'`:
|
||||
- git+https://github.com/pytest-dev/iniconfig@93f5930e668c0d1ddf4597e38dd0dea4e2665e7a
|
||||
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
||||
"###
|
||||
|
|
|
@ -1826,10 +1826,6 @@ fn update() -> Result<()> {
|
|||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
environment-markers = [
|
||||
"python_version <= '3.7'",
|
||||
"python_version > '3.7'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25 00:00:00 UTC"
|
||||
|
|
|
@ -8126,8 +8126,8 @@ fn unconditional_overlapping_marker_disjoint_version_constraints() -> Result<()>
|
|||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
× No solution found when resolving dependencies for split (python_version > '3.10'):
|
||||
╰─▶ Because only datasets{python_version > '3.10'}<2.19 is available and your project depends on datasets{python_version > '3.10'}>=2.19, we can conclude that your project's requirements are unsatisfiable.
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because your project depends on datasets<2.19 and datasets>=2.19, we can conclude that your project's requirements are unsatisfiable.
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
@ -9429,3 +9429,228 @@ fn lock_reorder() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_narrowed_python_version() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let dependency = context.temp_dir.child("dependency");
|
||||
dependency.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "dependency"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.7"
|
||||
dependencies = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.7"
|
||||
dependencies = ["dependency ; python_version < '3.9'", "dependency ; python_version > '3.10'"]
|
||||
[tool.uv.sources]
|
||||
dependency = { path = "./dependency" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let lockfile = context.temp_dir.join("uv.lock");
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
warning: `uv.sources` is experimental and may change without warning
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(&lockfile).unwrap();
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.7"
|
||||
environment-markers = [
|
||||
"python_version < '3.9'",
|
||||
"python_version >= '3.9' and python_version <= '3.10'",
|
||||
"python_version > '3.10'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25 00:00:00 UTC"
|
||||
|
||||
[[package]]
|
||||
name = "dependency"
|
||||
version = "0.1.0"
|
||||
source = { directory = "dependency" }
|
||||
dependencies = [
|
||||
{ name = "iniconfig", marker = "python_version < '3.9' or python_version > '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "iniconfig" }]
|
||||
|
||||
[[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 = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "dependency", marker = "python_version < '3.9' or python_version > '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dependency", marker = "python_version < '3.9'", directory = "dependency" },
|
||||
{ name = "dependency", marker = "python_version > '3.10'", directory = "dependency" },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
warning: `uv.sources` is experimental and may change without warning
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// When resolving, we should skip forks that have an upper-bound on Python version that's below
|
||||
/// our `requires-python` constraint.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/uv/issues/6059>
|
||||
#[test]
|
||||
fn lock_exclude_unnecessary_python_forks() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio ; sys_platform == 'darwin'",
|
||||
"anyio ; python_version > '3.10'"
|
||||
]
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Resolved 4 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"
|
||||
environment-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform != 'darwin'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25 00:00:00 UTC"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anyio", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "anyio", marker = "python_version > '3.10'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -8035,7 +8035,7 @@ fn universal_requires_python_incomplete() -> Result<()> {
|
|||
/// [2]: https://github.com/astral-sh/uv/pull/4707
|
||||
/// [3]: https://github.com/astral-sh/uv/pull/5597
|
||||
#[test]
|
||||
fn universal_no_repeated_unconditional_distributions() -> Result<()> {
|
||||
fn universal_no_repeated_unconditional_distributions_1() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(indoc::indoc! {r"
|
||||
|
@ -8135,6 +8135,60 @@ fn universal_no_repeated_unconditional_distributions() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// This test captures a case[1] that was broken by marker normalization.
|
||||
///
|
||||
/// [1]: https://github.com/astral-sh/uv/issues/6064
|
||||
#[test]
|
||||
fn universal_no_repeated_unconditional_distributions_2() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(indoc::indoc! {r"
|
||||
pylint
|
||||
dill==0.3.1.1
|
||||
"})?;
|
||||
|
||||
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
|
||||
.arg("requirements.in")
|
||||
.arg("-p")
|
||||
.arg("3.11")
|
||||
.arg("--universal"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.11 --universal
|
||||
astroid==2.13.5
|
||||
# via pylint
|
||||
colorama==0.4.6 ; sys_platform == 'win32'
|
||||
# via pylint
|
||||
dill==0.3.1.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# pylint
|
||||
isort==5.13.2
|
||||
# via pylint
|
||||
lazy-object-proxy==1.10.0
|
||||
# via astroid
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
platformdirs==4.2.0
|
||||
# via pylint
|
||||
pylint==2.15.8
|
||||
# via -r requirements.in
|
||||
tomlkit==0.12.4
|
||||
# via pylint
|
||||
wrapt==1.16.0
|
||||
# via astroid
|
||||
|
||||
----- stderr -----
|
||||
warning: The requested Python version 3.11 is not available; 3.12.[X] will be used to build dependencies instead.
|
||||
Resolved 10 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Solve for upper bounds before solving for lower bounds. A solution that satisfies `pylint < 3`
|
||||
/// can also work for `pylint > 2`, but the inverse isn't true (due to maximum version selection).
|
||||
#[test]
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,9 +5,8 @@ expression: lock
|
|||
version = 1
|
||||
requires-python = ">=3.11, <3.12"
|
||||
environment-markers = [
|
||||
"python_version < '3.11'",
|
||||
"python_version >= '3.11.[X]' and python_version < '3.12'",
|
||||
"python_version >= '3.12' and python_version < '3.13'",
|
||||
"python_version < '3.12'",
|
||||
"python_version < '3.13'",
|
||||
"python_version >= '3.13'",
|
||||
]
|
||||
|
||||
|
@ -878,7 +877,7 @@ dependencies = [
|
|||
{ name = "envier" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "setuptools", marker = "python_version >= '3.12'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "six" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "typing-extensions" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue