mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-25 05:33:43 +00:00
Implement marker trees using algebraic decision diagrams (#5898)
## Summary This PR rewrites the `MarkerTree` type to use algebraic decision diagrams (ADD). This has many benefits: - The diagram is canonical for a given marker function. It is impossible to create two functionally equivalent marker trees that don't refer to the same underlying ADD. This also means that any trivially true or unsatisfiable markers are represented by the same constants. - The diagram can handle complex operations (conjunction/disjunction) in polynomial time, as well as constant-time negation. - The diagram can be converted to a simplified DNF form for user-facing output. The new representation gives us a lot more confidence in our marker operations and simplification, which is proving to be very important (see https://github.com/astral-sh/uv/pull/5733 and https://github.com/astral-sh/uv/pull/5163). Unfortunately, it is not easy to split this PR into multiple commits because it is a large rewrite of the `marker` module. I'd suggest reading through the `marker/algebra.rs`, `marker/simplify.rs`, and `marker/tree.rs` files for the new implementation, as well as the updated snapshots to verify how the new simplification rules work in practice. However, a few other things were changed: - [We now use release-only comparisons for `python_full_version`, where we previously only did for `python_version`](https://github.com/astral-sh/uv/blob/ibraheem/canonical-markers/crates/pep508-rs/src/marker/algebra.rs#L522). I'm unsure how marker operations should work in the presence of pre-release versions if we decide that this is incorrect. - [Meaningless marker expressions are now ignored](https://github.com/astral-sh/uv/blob/ibraheem/canonical-markers/crates/pep508-rs/src/marker/parse.rs#L502). This means that a marker such as `'x' == 'x'` will always evaluate to `true` (as if the expression did not exist), whereas we previously treated this as always `false`. It's negation however, remains `false`. - [Unsatisfiable markers are written as `python_version < '0'`](https://github.com/astral-sh/uv/blob/ibraheem/canonical-markers/crates/pep508-rs/src/marker/tree.rs#L1329). - The `PubGrubSpecifier` type has been moved to the new `uv-pubgrub` crate, shared by `pep508-rs` and `uv-resolver`. `pep508-rs` also depends on the `pubgrub` crate for the `Range` type, we probably want to move `pubgrub::Range` into a separate crate to break this, but I don't think that should block this PR (cc @konstin). There is still some remaining work here that I decided to leave for now for the sake of unblocking some of the related work on the resolver. - We still use `Option<MarkerTree>` throughout uv, which is unnecessary now that `MarkerTree::TRUE` is canonical. - The `MarkerTree` type is now interned globally and can potentially implement `Copy`. However, it's unclear if we want to add more information to marker trees that would make it `!Copy`. For example, we may wish to attach extra and requires-python environment information to avoid simplifying after construction. - We don't currently combine `python_full_version` and `python_version` markers. - I also have not spent too much time investigating performance and there is probably some low-hanging fruit. Many of the test cases I did run actually saw large performance improvements due to the markers being simplified internally, reducing the stress on the old `normalize` routine, especially for the extremely large markers seen in `transformers` and other projects. Resolves https://github.com/astral-sh/uv/issues/5660, https://github.com/astral-sh/uv/issues/5179.
This commit is contained in:
parent
cdd7341b6d
commit
ffd18cc75d
39 changed files with 3258 additions and 2604 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -449,6 +449,12 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boxcar"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "510a90332002c1af3317ef6b712f0dab697f30bbe809b86965eac2923c0bca8e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
|
|
@ -2459,16 +2465,22 @@ dependencies = [
|
||||||
name = "pep508_rs"
|
name = "pep508_rs"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"boxcar",
|
||||||
"derivative",
|
"derivative",
|
||||||
|
"indexmap",
|
||||||
"insta",
|
"insta",
|
||||||
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
|
"pubgrub",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
"pyo3-log",
|
"pyo3-log",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rustc-hash 2.0.0",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"smallvec",
|
||||||
"testing_logger",
|
"testing_logger",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2476,6 +2488,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
|
"uv-pubgrub",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4937,6 +4950,16 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uv-pubgrub"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"pep440_rs",
|
||||||
|
"pubgrub",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uv-python"
|
name = "uv-python"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
@ -5067,6 +5090,7 @@ dependencies = [
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
"uv-git",
|
"uv-git",
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
|
"uv-pubgrub",
|
||||||
"uv-python",
|
"uv-python",
|
||||||
"uv-types",
|
"uv-types",
|
||||||
"uv-warnings",
|
"uv-warnings",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ uv-options-metadata = { path = "crates/uv-options-metadata" }
|
||||||
uv-python = { path = "crates/uv-python" }
|
uv-python = { path = "crates/uv-python" }
|
||||||
uv-requirements = { path = "crates/uv-requirements" }
|
uv-requirements = { path = "crates/uv-requirements" }
|
||||||
uv-resolver = { path = "crates/uv-resolver" }
|
uv-resolver = { path = "crates/uv-resolver" }
|
||||||
|
uv-pubgrub = { path = "crates/uv-pubgrub" }
|
||||||
uv-scripts = { path = "crates/uv-scripts" }
|
uv-scripts = { path = "crates/uv-scripts" }
|
||||||
uv-settings = { path = "crates/uv-settings" }
|
uv-settings = { path = "crates/uv-settings" }
|
||||||
uv-shell = { path = "crates/uv-shell" }
|
uv-shell = { path = "crates/uv-shell" }
|
||||||
|
|
@ -66,6 +67,7 @@ async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "011b
|
||||||
axoupdater = { version = "0.7.0", default-features = false }
|
axoupdater = { version = "0.7.0", default-features = false }
|
||||||
backoff = { version = "0.4.0" }
|
backoff = { version = "0.4.0" }
|
||||||
base64 = { version = "0.22.0" }
|
base64 = { version = "0.22.0" }
|
||||||
|
boxcar = { version = "0.2.5" }
|
||||||
cachedir = { version = "0.3.1" }
|
cachedir = { version = "0.3.1" }
|
||||||
cargo-util = { version = "0.2.8" }
|
cargo-util = { version = "0.2.8" }
|
||||||
chrono = { version = "0.4.31" }
|
chrono = { version = "0.4.31" }
|
||||||
|
|
@ -130,6 +132,7 @@ seahash = { version = "4.1.0" }
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.114" }
|
serde_json = { version = "1.0.114" }
|
||||||
sha2 = { version = "0.10.8" }
|
sha2 = { version = "0.10.8" }
|
||||||
|
smallvec = { version = "1.13.2" }
|
||||||
syn = { version = "2.0.66" }
|
syn = { version = "2.0.66" }
|
||||||
sys-info = { version = "0.9.1" }
|
sys-info = { version = "0.9.1" }
|
||||||
target-lexicon = {version = "0.12.14" }
|
target-lexicon = {version = "0.12.14" }
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,13 @@ impl VersionSpecifier {
|
||||||
version,
|
version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// `!=<version>`
|
||||||
|
pub fn not_equals_version(version: Version) -> Self {
|
||||||
|
Self {
|
||||||
|
operator: Operator::NotEqual,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `>=<version>`
|
/// `>=<version>`
|
||||||
pub fn greater_than_equal_version(version: Version) -> Self {
|
pub fn greater_than_equal_version(version: Version) -> Self {
|
||||||
|
|
@ -413,6 +420,29 @@ impl VersionSpecifier {
|
||||||
version,
|
version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// `><version>`
|
||||||
|
pub fn greater_than_version(version: Version) -> Self {
|
||||||
|
Self {
|
||||||
|
operator: Operator::GreaterThan,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `<=<version>`
|
||||||
|
pub fn less_than_equal_version(version: Version) -> Self {
|
||||||
|
Self {
|
||||||
|
operator: Operator::LessThanEqual,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `<<version>`
|
||||||
|
pub fn less_than_version(version: Version) -> Self {
|
||||||
|
Self {
|
||||||
|
operator: Operator::LessThan,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the operator, e.g. `>=` in `>= 2.0.0`
|
/// Get the operator, e.g. `>=` in `>= 2.0.0`
|
||||||
pub fn operator(&self) -> &Operator {
|
pub fn operator(&self) -> &Operator {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,12 @@ crate-type = ["cdylib", "rlib"]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
boxcar = { workspace = true }
|
||||||
derivative = { workspace = true }
|
derivative = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
|
pubgrub = { workspace = true }
|
||||||
|
rustc-hash = { workspace = true }
|
||||||
pep440_rs = { workspace = true }
|
pep440_rs = { workspace = true }
|
||||||
pyo3 = { workspace = true, optional = true, features = ["abi3", "extension-module"] }
|
pyo3 = { workspace = true, optional = true, features = ["abi3", "extension-module"] }
|
||||||
pyo3-log = { workspace = true, optional = true }
|
pyo3-log = { workspace = true, optional = true }
|
||||||
|
|
@ -28,12 +33,14 @@ regex = { workspace = true }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, features = ["derive", "rc"] }
|
serde = { workspace = true, features = ["derive", "rc"] }
|
||||||
serde_json = { workspace = true, optional = true }
|
serde_json = { workspace = true, optional = true }
|
||||||
|
smallvec = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true, optional = true }
|
tracing = { workspace = true, optional = true }
|
||||||
unicode-width = { workspace = true }
|
unicode-width = { workspace = true }
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
|
uv-pubgrub = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { version = "1.36.1" }
|
insta = { version = "1.36.1" }
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,10 @@ use url::Url;
|
||||||
|
|
||||||
use cursor::Cursor;
|
use cursor::Cursor;
|
||||||
pub use marker::{
|
pub use marker::{
|
||||||
ExtraOperator, MarkerEnvironment, MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator,
|
ContainsMarkerTree, ExtraMarkerTree, ExtraOperator, InMarkerTree, MarkerEnvironment,
|
||||||
MarkerTree, MarkerValue, MarkerValueString, MarkerValueVersion, MarkerWarningKind,
|
MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator, MarkerTree, MarkerTreeContents,
|
||||||
StringVersion,
|
MarkerTreeKind, MarkerValue, MarkerValueString, MarkerValueVersion, MarkerWarningKind,
|
||||||
|
StringMarkerTree, StringVersion, VersionMarkerTree,
|
||||||
};
|
};
|
||||||
pub use origin::RequirementOrigin;
|
pub use origin::RequirementOrigin;
|
||||||
#[cfg(feature = "pyo3")]
|
#[cfg(feature = "pyo3")]
|
||||||
|
|
@ -189,7 +190,7 @@ impl<T: Pep508Url + Display> Display for Requirement<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(marker) = &self.marker {
|
if let Some(marker) = self.marker.as_ref().and_then(MarkerTree::contents) {
|
||||||
write!(f, " ; {marker}")?;
|
write!(f, " ; {marker}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -255,7 +256,10 @@ impl PyRequirement {
|
||||||
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`
|
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`
|
||||||
#[getter]
|
#[getter]
|
||||||
pub fn marker(&self) -> Option<String> {
|
pub fn marker(&self) -> Option<String> {
|
||||||
self.marker.as_ref().map(ToString::to_string)
|
self.marker
|
||||||
|
.as_ref()
|
||||||
|
.and_then(MarkerTree::contents)
|
||||||
|
.map(|marker| marker.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a PEP 440 string
|
/// Parses a PEP 440 string
|
||||||
|
|
@ -416,18 +420,20 @@ impl<T: Pep508Url> Requirement<T> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_extra_marker(self, extra: &ExtraName) -> Self {
|
pub fn with_extra_marker(self, extra: &ExtraName) -> Self {
|
||||||
let marker = match self.marker {
|
let marker = match self.marker {
|
||||||
Some(expression) => MarkerTree::And(vec![
|
Some(mut marker) => {
|
||||||
expression,
|
let extra = MarkerTree::expression(MarkerExpression::Extra {
|
||||||
MarkerTree::Expression(MarkerExpression::Extra {
|
|
||||||
operator: ExtraOperator::Equal,
|
operator: ExtraOperator::Equal,
|
||||||
name: extra.clone(),
|
name: extra.clone(),
|
||||||
}),
|
});
|
||||||
]),
|
marker.and(extra);
|
||||||
None => MarkerTree::Expression(MarkerExpression::Extra {
|
marker
|
||||||
|
}
|
||||||
|
None => MarkerTree::expression(MarkerExpression::Extra {
|
||||||
operator: ExtraOperator::Equal,
|
operator: ExtraOperator::Equal,
|
||||||
name: extra.clone(),
|
name: extra.clone(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
marker: Some(marker),
|
marker: Some(marker),
|
||||||
..self
|
..self
|
||||||
|
|
@ -1043,7 +1049,7 @@ fn parse_pep508_requirement<T: Pep508Url>(
|
||||||
let marker = if cursor.peek_char() == Some(';') {
|
let marker = if cursor.peek_char() == Some(';') {
|
||||||
// Skip past the semicolon
|
// Skip past the semicolon
|
||||||
cursor.next();
|
cursor.next();
|
||||||
Some(marker::parse::parse_markers_cursor(cursor, reporter)?)
|
marker::parse::parse_markers_cursor(cursor, reporter)?
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -1123,10 +1129,10 @@ mod tests {
|
||||||
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
||||||
|
|
||||||
use crate::cursor::Cursor;
|
use crate::cursor::Cursor;
|
||||||
use crate::marker::{
|
use crate::marker::{parse, MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||||
parse, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString, MarkerValueVersion,
|
use crate::{
|
||||||
|
MarkerOperator, MarkerValueString, Requirement, TracingReporter, VerbatimUrl, VersionOrUrl,
|
||||||
};
|
};
|
||||||
use crate::{Requirement, TracingReporter, VerbatimUrl, VersionOrUrl};
|
|
||||||
|
|
||||||
fn parse_pep508_err(input: &str) -> String {
|
fn parse_pep508_err(input: &str) -> String {
|
||||||
Requirement::<VerbatimUrl>::from_str(input)
|
Requirement::<VerbatimUrl>::from_str(input)
|
||||||
|
|
@ -1216,7 +1222,7 @@ mod tests {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
)),
|
)),
|
||||||
marker: Some(MarkerTree::Expression(MarkerExpression::Version {
|
marker: Some(MarkerTree::expression(MarkerExpression::Version {
|
||||||
key: MarkerValueVersion::PythonVersion,
|
key: MarkerValueVersion::PythonVersion,
|
||||||
specifier: VersionSpecifier::from_pattern(
|
specifier: VersionSpecifier::from_pattern(
|
||||||
pep440_rs::Operator::LessThan,
|
pep440_rs::Operator::LessThan,
|
||||||
|
|
@ -1463,37 +1469,38 @@ mod tests {
|
||||||
&mut Cursor::new(marker),
|
&mut Cursor::new(marker),
|
||||||
&mut TracingReporter,
|
&mut TracingReporter,
|
||||||
)
|
)
|
||||||
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let expected = MarkerTree::And(vec![
|
|
||||||
MarkerTree::Expression(MarkerExpression::Version {
|
let mut a = MarkerTree::expression(MarkerExpression::Version {
|
||||||
key: MarkerValueVersion::PythonVersion,
|
key: MarkerValueVersion::PythonVersion,
|
||||||
specifier: VersionSpecifier::from_pattern(
|
specifier: VersionSpecifier::from_pattern(
|
||||||
pep440_rs::Operator::Equal,
|
pep440_rs::Operator::Equal,
|
||||||
"2.7".parse().unwrap(),
|
"2.7".parse().unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
}),
|
});
|
||||||
MarkerTree::Or(vec![
|
let mut b = MarkerTree::expression(MarkerExpression::String {
|
||||||
MarkerTree::Expression(MarkerExpression::String {
|
key: MarkerValueString::SysPlatform,
|
||||||
key: MarkerValueString::SysPlatform,
|
operator: MarkerOperator::Equal,
|
||||||
operator: MarkerOperator::Equal,
|
value: "win32".to_string(),
|
||||||
value: "win32".to_string(),
|
});
|
||||||
}),
|
let mut c = MarkerTree::expression(MarkerExpression::String {
|
||||||
MarkerTree::And(vec![
|
key: MarkerValueString::OsName,
|
||||||
MarkerTree::Expression(MarkerExpression::String {
|
operator: MarkerOperator::Equal,
|
||||||
key: MarkerValueString::OsName,
|
value: "linux".to_string(),
|
||||||
operator: MarkerOperator::Equal,
|
});
|
||||||
value: "linux".to_string(),
|
let d = MarkerTree::expression(MarkerExpression::String {
|
||||||
}),
|
key: MarkerValueString::ImplementationName,
|
||||||
MarkerTree::Expression(MarkerExpression::String {
|
operator: MarkerOperator::Equal,
|
||||||
key: MarkerValueString::ImplementationName,
|
value: "cpython".to_string(),
|
||||||
operator: MarkerOperator::Equal,
|
});
|
||||||
value: "cpython".to_string(),
|
|
||||||
}),
|
c.and(d);
|
||||||
]),
|
b.or(c);
|
||||||
]),
|
a.and(b);
|
||||||
]);
|
|
||||||
assert_eq!(expected, actual);
|
assert_eq!(a, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
894
crates/pep508-rs/src/marker/algebra.rs
Normal file
894
crates/pep508-rs/src/marker/algebra.rs
Normal file
|
|
@ -0,0 +1,894 @@
|
||||||
|
//! This module implements marker tree operations using Algebraic Decision Diagrams (ADD).
|
||||||
|
//!
|
||||||
|
//! An ADD is a tree of decision nodes as well as two terminal nodes, `true` and `false`. Marker
|
||||||
|
//! variables are represented as decision nodes. The edge from a decision node to it's child
|
||||||
|
//! represents a particular assignment of a value to that variable. Depending on the type of
|
||||||
|
//! variable, an edge can be represented by binary values or a disjoint set of ranges, as opposed
|
||||||
|
//! to a traditional Binary Decision Diagram.
|
||||||
|
//!
|
||||||
|
//! For example, the marker `python_version > '3.7' and os_name == 'Linux'` creates the following
|
||||||
|
//! marker tree:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! python_version:
|
||||||
|
//! (> '3.7') -> os_name:
|
||||||
|
//! (> 'Linux') -> FALSE
|
||||||
|
//! (== 'Linux') -> TRUE
|
||||||
|
//! (< 'Linux') -> FALSE
|
||||||
|
//! (<= '3.7') -> FALSE
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Specifically, a marker tree is represented as a Reduced Ordered ADD. An ADD is ordered if
|
||||||
|
//! different variables appear in the same order on all paths from the root. Additionally, an ADD
|
||||||
|
//! is reduced if:
|
||||||
|
//! - Isomorphic nodes are merged.
|
||||||
|
//! - Nodes with isomorphic children are eliminated.
|
||||||
|
//!
|
||||||
|
//! These two rules provide an important guarantee for marker trees: marker trees are canonical for
|
||||||
|
//! a given marker function and variable ordering. Because variable ordering is defined at compile-time,
|
||||||
|
//! this means any functionally equivalent marker trees are normalized upon construction. Importantly,
|
||||||
|
//! this means that we can identify trivially true marker trees, as well as unsatisfiable marker trees.
|
||||||
|
//! This provides important information to the resolver when forking.
|
||||||
|
//!
|
||||||
|
//! ADDs provide polynomial time operations such as conjunction and negation, which is important as marker
|
||||||
|
//! trees are combined during universal resolution. Because ADDs solve the SAT problem, constructing an
|
||||||
|
//! arbitrary ADD can theoretically take exponential time in the worst case. However, in practice, marker trees
|
||||||
|
//! have a limited number of variables and user-provided marker trees are typically very simple.
|
||||||
|
//!
|
||||||
|
//! Additionally, the implementation in this module uses complemented edges, meaning a marker tree and
|
||||||
|
//! it's complement are represented by the same node internally. This allows cheap constant-time marker
|
||||||
|
//! tree negation. It also allows us to only implement a single operation for both `AND` and `OR`, implementing
|
||||||
|
//! the other in terms of its De Morgan Complement.
|
||||||
|
//!
|
||||||
|
//! ADDs are created and managed through the global [`Interner`]. A given ADD is referenced through
|
||||||
|
//! a [`NodeId`], which represents a potentially complemented reference to a [`Node`] in the interner,
|
||||||
|
//! or a terminal `true`/`false` node. Interning allows the reduction rule that isomorphic nodes are
|
||||||
|
//! merged to be applied globally.
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Bound;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::MutexGuard;
|
||||||
|
|
||||||
|
use itertools::Either;
|
||||||
|
use pep440_rs::{Version, VersionSpecifier};
|
||||||
|
use pubgrub::Range;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use uv_normalize::ExtraName;
|
||||||
|
use uv_pubgrub::PubGrubSpecifier;
|
||||||
|
|
||||||
|
use crate::ExtraOperator;
|
||||||
|
use crate::{MarkerExpression, MarkerOperator, MarkerValueString, MarkerValueVersion};
|
||||||
|
|
||||||
|
/// The global node interner.
|
||||||
|
pub(crate) static INTERNER: LazyLock<Interner> = LazyLock::new(Interner::default);
|
||||||
|
|
||||||
|
/// An interner for decision nodes.
|
||||||
|
///
|
||||||
|
/// Interning decision nodes allows isomorphic nodes to be automatically merged.
|
||||||
|
/// It also allows nodes to cheaply compared.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct Interner {
|
||||||
|
pub(crate) shared: InternerShared,
|
||||||
|
state: Mutex<InternerState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shared part of an [`Interner`], which can be accessed without a lock.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct InternerShared {
|
||||||
|
/// A list of unique [`Node`]s.
|
||||||
|
nodes: boxcar::Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mutable [`Interner`] state, stored behind a lock.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct InternerState {
|
||||||
|
/// A map from a [`Node`] to a unique [`NodeId`], representing an index
|
||||||
|
/// into [`InternerShared`].
|
||||||
|
unique: FxHashMap<Node, NodeId>,
|
||||||
|
|
||||||
|
/// A cache for `AND` operations between two nodes.
|
||||||
|
/// Note that `OR` is implemented in terms of `AND`.
|
||||||
|
cache: FxHashMap<(NodeId, NodeId), NodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InternerShared {
|
||||||
|
/// Returns the node for the given [`NodeId`].
|
||||||
|
pub(crate) fn node(&self, id: NodeId) -> &Node {
|
||||||
|
&self.nodes[id.index()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interner {
|
||||||
|
/// Locks the interner state, returning a guard that can be used to perform marker
|
||||||
|
/// operations.
|
||||||
|
pub(crate) fn lock(&self) -> InternerGuard<'_> {
|
||||||
|
InternerGuard {
|
||||||
|
state: self.state.lock().unwrap(),
|
||||||
|
shared: &self.shared,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A lock of [`InternerState`].
|
||||||
|
pub(crate) struct InternerGuard<'a> {
|
||||||
|
state: MutexGuard<'a, InternerState>,
|
||||||
|
shared: &'a InternerShared,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InternerGuard<'_> {
|
||||||
|
/// Creates a decision node with the given variable and children.
|
||||||
|
fn create_node(&mut self, var: Variable, children: Edges) -> NodeId {
|
||||||
|
let mut node = Node { var, children };
|
||||||
|
let mut first = node.children.nodes().next().unwrap();
|
||||||
|
|
||||||
|
// With a complemented edge representation, there are two ways to represent the same node:
|
||||||
|
// complementing the root and all children edges results in the same node. To ensure markers
|
||||||
|
// are canonical, the first child edge is never complemented.
|
||||||
|
let mut flipped = false;
|
||||||
|
if first.is_complement() {
|
||||||
|
node = node.not();
|
||||||
|
first = first.not();
|
||||||
|
flipped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduction: If all children refer to the same node, we eliminate the parent node
|
||||||
|
// and just return the child.
|
||||||
|
if node.children.nodes().all(|node| node == first) {
|
||||||
|
return if flipped { first.not() } else { first };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the node.
|
||||||
|
let id = self
|
||||||
|
.state
|
||||||
|
.unique
|
||||||
|
.entry(node.clone())
|
||||||
|
.or_insert_with(|| NodeId::new(self.shared.nodes.push(node), false));
|
||||||
|
|
||||||
|
if flipped {
|
||||||
|
id.not()
|
||||||
|
} else {
|
||||||
|
*id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a decision node for a single marker expression.
|
||||||
|
pub(crate) fn expression(&mut self, expr: MarkerExpression) -> NodeId {
|
||||||
|
let (var, children) = match expr {
|
||||||
|
// A variable representing the output of a version key. Edges correspond
|
||||||
|
// to disjoint version ranges.
|
||||||
|
MarkerExpression::Version { key, specifier } => {
|
||||||
|
(Variable::Version(key), Edges::from_specifier(&specifier))
|
||||||
|
}
|
||||||
|
// The `in` and `contains` operators are a bit different than other operators.
|
||||||
|
// In particular, they do not represent a particular value for the corresponding
|
||||||
|
// variable, and can overlap. For example, `'nux' in os_name` and `os_name == 'Linux'`
|
||||||
|
// can both be `true` in the same marker environment, and so cannot be represented by
|
||||||
|
// the same variable. Because of this, we represent `in` and `contains`, as well as
|
||||||
|
// their negations, as distinct variables, unrelated to the range of a given key.
|
||||||
|
//
|
||||||
|
// Note that in the presence of the `in` operator, we may not be able to simplify
|
||||||
|
// some marker trees to a constant `true` or `false`. For example, it is not trivial to
|
||||||
|
// detect that `os_name > 'z' and os_name in 'Linux'` is unsatisfiable.
|
||||||
|
MarkerExpression::String {
|
||||||
|
key,
|
||||||
|
operator: MarkerOperator::In,
|
||||||
|
value,
|
||||||
|
} => (Variable::In { key, value }, Edges::from_bool(true)),
|
||||||
|
MarkerExpression::String {
|
||||||
|
key,
|
||||||
|
operator: MarkerOperator::NotIn,
|
||||||
|
value,
|
||||||
|
} => (Variable::In { key, value }, Edges::from_bool(false)),
|
||||||
|
MarkerExpression::String {
|
||||||
|
key,
|
||||||
|
operator: MarkerOperator::Contains,
|
||||||
|
value,
|
||||||
|
} => (Variable::Contains { key, value }, Edges::from_bool(true)),
|
||||||
|
MarkerExpression::String {
|
||||||
|
key,
|
||||||
|
operator: MarkerOperator::NotContains,
|
||||||
|
value,
|
||||||
|
} => (Variable::Contains { key, value }, Edges::from_bool(false)),
|
||||||
|
// A variable representing the output of a string key. Edges correspond
|
||||||
|
// to disjoint string ranges.
|
||||||
|
MarkerExpression::String {
|
||||||
|
key,
|
||||||
|
operator,
|
||||||
|
value,
|
||||||
|
} => (Variable::String(key), Edges::from_string(operator, value)),
|
||||||
|
// A variable representing the existence or absence of a particular extra.
|
||||||
|
MarkerExpression::Extra {
|
||||||
|
name,
|
||||||
|
operator: ExtraOperator::Equal,
|
||||||
|
} => (Variable::Extra(name), Edges::from_bool(true)),
|
||||||
|
MarkerExpression::Extra {
|
||||||
|
name,
|
||||||
|
operator: ExtraOperator::NotEqual,
|
||||||
|
} => (Variable::Extra(name), Edges::from_bool(false)),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.create_node(var, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a decision node representing the disjunction of two nodes.
|
||||||
|
pub(crate) fn or(&mut self, x: NodeId, y: NodeId) -> NodeId {
|
||||||
|
// We take advantage of cheap negation here and implement OR in terms
|
||||||
|
// of it's De Morgan complement.
|
||||||
|
self.and(x.not(), y.not()).not()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a decision node representing the conjunction of two nodes.
|
||||||
|
pub(crate) fn and(&mut self, xi: NodeId, yi: NodeId) -> NodeId {
|
||||||
|
if xi == NodeId::TRUE {
|
||||||
|
return yi;
|
||||||
|
}
|
||||||
|
if yi == NodeId::TRUE {
|
||||||
|
return xi;
|
||||||
|
}
|
||||||
|
if xi == yi {
|
||||||
|
return xi;
|
||||||
|
}
|
||||||
|
if xi == NodeId::FALSE || yi == NodeId::FALSE {
|
||||||
|
return NodeId::FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// X and Y are not equal but refer to the same node.
|
||||||
|
// Thus one is complement but not the other (X and not X).
|
||||||
|
if xi.index() == yi.index() {
|
||||||
|
return NodeId::FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The operation was memoized.
|
||||||
|
if let Some(result) = self.state.cache.get(&(xi, yi)) {
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x, y) = (self.shared.node(xi), self.shared.node(yi));
|
||||||
|
|
||||||
|
// Perform Shannon Expansion of the higher order variable.
|
||||||
|
let (func, children) = match x.var.cmp(&y.var) {
|
||||||
|
// X is higher order than Y, apply Y to every child of X.
|
||||||
|
Ordering::Less => {
|
||||||
|
let children = x.children.map(xi, |node| self.and(node, yi));
|
||||||
|
(x.var.clone(), children)
|
||||||
|
}
|
||||||
|
// Y is higher order than X, apply X to every child of Y.
|
||||||
|
Ordering::Greater => {
|
||||||
|
let children = y.children.map(yi, |node| self.and(node, xi));
|
||||||
|
(y.var.clone(), children)
|
||||||
|
}
|
||||||
|
// X and Y represent the same variable, merge their children.
|
||||||
|
Ordering::Equal => {
|
||||||
|
let children = x.children.apply(xi, &y.children, yi, |x, y| self.and(x, y));
|
||||||
|
(x.var.clone(), children)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the output node.
|
||||||
|
let node = self.create_node(func, children);
|
||||||
|
|
||||||
|
// Memoize the result of this operation.
|
||||||
|
//
|
||||||
|
// ADDs often contain duplicated subgraphs in distinct branches due to the restricted
|
||||||
|
// variable ordering. Memoizing allows ADD operations to remain polynomial time.
|
||||||
|
self.state.cache.insert((xi, yi), node);
|
||||||
|
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict the output of a given boolean variable in the tree.
|
||||||
|
//
|
||||||
|
// If the provided function `f` returns a `Some` boolean value, the tree will be simplified
|
||||||
|
// with the assumption that the given variable is restricted to that value. If the function
|
||||||
|
// returns `None`, the variable will not be affected.
|
||||||
|
pub(crate) fn restrict(&mut self, i: NodeId, f: &impl Fn(&Variable) -> Option<bool>) -> NodeId {
|
||||||
|
if matches!(i, NodeId::TRUE | NodeId::FALSE) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = self.shared.node(i);
|
||||||
|
if let Edges::Boolean { high, low } = node.children {
|
||||||
|
if let Some(value) = f(&node.var) {
|
||||||
|
// Restrict this variable to the given output by merging it
|
||||||
|
// with the relevant child.
|
||||||
|
let node = if value { high } else { low };
|
||||||
|
return node.negate(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict all nodes recursively.
|
||||||
|
let children = node.children.map(i, |node| self.restrict(node, f));
|
||||||
|
self.create_node(node.var.clone(), children)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict the output of a given version variable in the tree.
|
||||||
|
//
|
||||||
|
// If the provided function `f` returns a `Some` range, the tree will be simplified with
|
||||||
|
// the assumption that the given variable is restricted to values in that range. If the function
|
||||||
|
// returns `None`, the variable will not be affected.
|
||||||
|
pub(crate) fn restrict_versions(
|
||||||
|
&mut self,
|
||||||
|
i: NodeId,
|
||||||
|
f: &impl Fn(&Variable) -> Option<Range<Version>>,
|
||||||
|
) -> NodeId {
|
||||||
|
if matches!(i, NodeId::TRUE | NodeId::FALSE) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = self.shared.node(i);
|
||||||
|
if let Edges::Version { edges: ref map } = node.children {
|
||||||
|
if let Some(allowed) = f(&node.var) {
|
||||||
|
// Restrict the output of this variable to the given range.
|
||||||
|
let mut simplified = SmallVec::new();
|
||||||
|
for (range, node) in map {
|
||||||
|
let restricted = range.intersection(&allowed);
|
||||||
|
if restricted.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
simplified.push((restricted.clone(), *node));
|
||||||
|
}
|
||||||
|
|
||||||
|
return self
|
||||||
|
.create_node(node.var.clone(), Edges::Version { edges: simplified })
|
||||||
|
.negate(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict all nodes recursively.
|
||||||
|
let children = node.children.map(i, |node| self.restrict_versions(node, f));
|
||||||
|
self.create_node(node.var.clone(), children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A unique variable for a decision node.
|
||||||
|
///
|
||||||
|
/// This `enum` also defines the variable ordering for all ADDs.
|
||||||
|
/// Variable ordering is an interesting property of ADDs. A bad ordering
|
||||||
|
/// can lead to exponential explosion of the size of an ADD. However,
|
||||||
|
/// dynamically computing an optimal ordering is NP-complete.
|
||||||
|
///
|
||||||
|
/// We may wish to investigate the effect of this ordering on common marker
|
||||||
|
/// trees. However, marker trees are typically small, so this may not be high
|
||||||
|
/// impact.
|
||||||
|
#[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Debug)]
|
||||||
|
pub(crate) enum Variable {
|
||||||
|
/// A version marker, such as `python_version`.
|
||||||
|
///
|
||||||
|
/// This is the highest order variable as it typically contains the most complex
|
||||||
|
/// ranges, allowing us to merge ranges at the top-level.
|
||||||
|
Version(MarkerValueVersion),
|
||||||
|
/// A string marker, such as `os_name`.
|
||||||
|
String(MarkerValueString),
|
||||||
|
/// A variable representing a `<key> in <value>` expression for a particular
|
||||||
|
/// string marker and value.
|
||||||
|
In {
|
||||||
|
key: MarkerValueString,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// A variable representing a `<value> in <key>` expression for a particular
|
||||||
|
/// string marker and value.
|
||||||
|
Contains {
|
||||||
|
key: MarkerValueString,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// A variable representing the existence or absence of a given extra.
|
||||||
|
///
|
||||||
|
/// We keep extras at the leaves of the tree, so when simplifying extras we can
|
||||||
|
/// trivially remove the leaves without having to reconstruct the entire tree.
|
||||||
|
Extra(ExtraName),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A decision node in an Algebraic Decision Diagram.
|
||||||
|
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||||
|
pub(crate) struct Node {
|
||||||
|
/// The variable this node represents.
|
||||||
|
pub(crate) var: Variable,
|
||||||
|
/// The children of this node, with edges representing the possible outputs
|
||||||
|
/// of this variable.
|
||||||
|
pub(crate) children: Edges,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
/// Return the complement of this node, flipping all children IDs.
|
||||||
|
fn not(self) -> Node {
|
||||||
|
Node {
|
||||||
|
var: self.var,
|
||||||
|
children: self.children.not(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ID representing a reference to a decision node in the [`Interner`].
|
||||||
|
///
|
||||||
|
/// The lowest bit of the ID is used represent complemented edges.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub(crate) struct NodeId(usize);
|
||||||
|
|
||||||
|
impl NodeId {
|
||||||
|
// The terminal node representing `true`, or a trivially `true` node.
|
||||||
|
pub(crate) const TRUE: NodeId = NodeId(0);
|
||||||
|
|
||||||
|
// The terminal node representing `false`, or an unsatisifable node.
|
||||||
|
pub(crate) const FALSE: NodeId = NodeId(1);
|
||||||
|
|
||||||
|
/// Create a new, optionally complemented, [`NodeId`] with the given index.
|
||||||
|
fn new(index: usize, complement: bool) -> NodeId {
|
||||||
|
// Ensure the index does not interfere with the lowest complement bit.
|
||||||
|
let index = (index + 1) << 1;
|
||||||
|
NodeId(index | usize::from(complement))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index of this ID, ignoring the complemented edge.
|
||||||
|
fn index(self) -> usize {
|
||||||
|
// Ignore the lowest bit and bring indices back to starting at `0`.
|
||||||
|
(self.0 >> 1) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if this ID represents a complemented edge.
|
||||||
|
fn is_complement(self) -> bool {
|
||||||
|
// Whether the lowest bit is set.
|
||||||
|
(self.0 & 1) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complement of this node.
|
||||||
|
pub(crate) fn not(self) -> NodeId {
|
||||||
|
// Toggle the lowest bit.
|
||||||
|
NodeId(self.0 ^ 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complement of this node, if it's parent is complemented.
|
||||||
|
///
|
||||||
|
/// This method is useful to restore the complemented state of children nodes
|
||||||
|
/// when traversing the tree.
|
||||||
|
pub(crate) fn negate(self, parent: NodeId) -> NodeId {
|
||||||
|
if parent.is_complement() {
|
||||||
|
self.not()
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if this node represents an unsatisfiable node.
|
||||||
|
pub(crate) fn is_false(self) -> bool {
|
||||||
|
self == NodeId::FALSE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if this node represents a trivially `true` node.
|
||||||
|
pub(crate) fn is_true(self) -> bool {
|
||||||
|
self == NodeId::TRUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`SmallVec`] with enough elements to hold two constant edges, as well as the
|
||||||
|
/// ranges in-between.
|
||||||
|
type SmallVec<T> = smallvec::SmallVec<[T; 5]>;
|
||||||
|
|
||||||
|
/// The edges of a decision node.
|
||||||
|
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||||
|
#[allow(clippy::large_enum_variant)] // Nodes are interned.
|
||||||
|
pub(crate) enum Edges {
|
||||||
|
// The edges of a version variable, representing a disjoint set of ranges that cover
|
||||||
|
// the output space.
|
||||||
|
//
|
||||||
|
// Invariant: All ranges are simple, meaning they can be represented by a bounded
|
||||||
|
// interval without gaps. Additionally, there are at least two edges in the set.
|
||||||
|
Version {
|
||||||
|
edges: SmallVec<(Range<Version>, NodeId)>,
|
||||||
|
},
|
||||||
|
// The edges of a string variable, representing a disjoint set of ranges that cover
|
||||||
|
// the output space.
|
||||||
|
//
|
||||||
|
// Invariant: All ranges are simple, meaning they can be represented by a bounded
|
||||||
|
// interval without gaps. Additionally, there are at least two edges in the set.
|
||||||
|
String {
|
||||||
|
edges: SmallVec<(Range<String>, NodeId)>,
|
||||||
|
},
|
||||||
|
// The edges of a boolean variable, representing the values `true` (the `high` child)
|
||||||
|
// and `false` (the `low` child).
|
||||||
|
Boolean {
|
||||||
|
high: NodeId,
|
||||||
|
low: NodeId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Edges {
|
||||||
|
/// Returns the [`Edges`] for a boolean variable.
|
||||||
|
fn from_bool(complemented: bool) -> Edges {
|
||||||
|
if complemented {
|
||||||
|
Edges::Boolean {
|
||||||
|
high: NodeId::TRUE,
|
||||||
|
low: NodeId::FALSE,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Edges::Boolean {
|
||||||
|
high: NodeId::FALSE,
|
||||||
|
low: NodeId::TRUE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`Edges`] for a string expression.
|
||||||
|
///
|
||||||
|
/// This function will panic for the `In` and `Contains` marker operators, which
|
||||||
|
/// should be represented as separate boolean variables.
|
||||||
|
fn from_string(operator: MarkerOperator, value: String) -> Edges {
|
||||||
|
let range: Range<String> = match operator {
|
||||||
|
MarkerOperator::Equal => Range::singleton(value),
|
||||||
|
MarkerOperator::NotEqual => Range::singleton(value).complement(),
|
||||||
|
MarkerOperator::GreaterThan => Range::strictly_higher_than(value),
|
||||||
|
MarkerOperator::GreaterEqual => Range::higher_than(value),
|
||||||
|
MarkerOperator::LessThan => Range::strictly_lower_than(value),
|
||||||
|
MarkerOperator::LessEqual => Range::lower_than(value),
|
||||||
|
MarkerOperator::TildeEqual => unreachable!("string comparisons with ~= are ignored"),
|
||||||
|
_ => unreachable!("`in` and `contains` are treated as boolean variables"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Edges::String {
|
||||||
|
edges: Edges::from_range(&range),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`Edges`] for a version specifier.
|
||||||
|
fn from_specifier(specifier: &VersionSpecifier) -> Edges {
|
||||||
|
// The decision diagram relies on the assumption that the negation of a marker tree is
|
||||||
|
// the complement of the marker space. However, pre-release versions violate this assumption.
|
||||||
|
// For example, the marker `python_full_version > '3.9' or python_full_version <= '3.9'`
|
||||||
|
// does not match `python_full_version == 3.9.0a0`. However, it's negation,
|
||||||
|
// `python_full_version > '3.9' and python_full_version <= '3.9'` also does not include
|
||||||
|
// `3.9.0a0`, and is actually `false`.
|
||||||
|
//
|
||||||
|
// For this reason we ignore pre-release versions entirely when evaluating markers.
|
||||||
|
// Note that `python_version` cannot take on pre-release values so this is necessary for
|
||||||
|
// simplifying ranges, but for `python_full_version` this decision is a semantic change.
|
||||||
|
let specifier = PubGrubSpecifier::from_release_specifier(specifier).unwrap();
|
||||||
|
Edges::Version {
|
||||||
|
edges: Edges::from_range(&specifier.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an [`Edges`] where values in the given range are `true`.
|
||||||
|
fn from_range<T>(range: &Range<T>) -> SmallVec<(Range<T>, NodeId)>
|
||||||
|
where
|
||||||
|
T: Ord + Clone,
|
||||||
|
{
|
||||||
|
let mut edges = SmallVec::new();
|
||||||
|
|
||||||
|
// Add the `true` edges.
|
||||||
|
for (start, end) in range.iter() {
|
||||||
|
let range = Range::from_range_bounds((start.clone(), end.clone()));
|
||||||
|
edges.push((range, NodeId::TRUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the `false` edges.
|
||||||
|
for (start, end) in range.complement().iter() {
|
||||||
|
let range = Range::from_range_bounds((start.clone(), end.clone()));
|
||||||
|
edges.push((range, NodeId::FALSE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the ranges.
|
||||||
|
//
|
||||||
|
// The ranges are disjoint so we don't care about equality.
|
||||||
|
edges.sort_by(|(range1, _), (range2, _)| compare_disjoint_range_start(range1, range2));
|
||||||
|
edges
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge two [`Edges`], applying the given operation (e.g., `AND` or `OR`) to all intersecting edges.
|
||||||
|
///
|
||||||
|
/// For example, given two nodes corresponding to the same boolean variable:
|
||||||
|
/// ```text
|
||||||
|
/// left (extra == 'foo'): { true: A, false: B }
|
||||||
|
/// right (extra == 'foo'): { true: C, false: D }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// We merge them into a single node by applying the given operation to the matching edges.
|
||||||
|
/// ```text
|
||||||
|
/// (extra == 'foo'): { true: (A and C), false: (B and D) }
|
||||||
|
/// ```
|
||||||
|
/// For non-boolean variables, this is more complex. See `apply_ranges` for details.
|
||||||
|
///
|
||||||
|
/// Note that the LHS and RHS must be of the same [`Edges`] variant.
|
||||||
|
fn apply(
|
||||||
|
&self,
|
||||||
|
parent: NodeId,
|
||||||
|
right_edges: &Edges,
|
||||||
|
right_parent: NodeId,
|
||||||
|
mut apply: impl FnMut(NodeId, NodeId) -> NodeId,
|
||||||
|
) -> Edges {
|
||||||
|
match (self, right_edges) {
|
||||||
|
// For version or string variables, we have to split and merge the overlapping ranges.
|
||||||
|
(Edges::Version { edges }, Edges::Version { edges: right_edges }) => Edges::Version {
|
||||||
|
edges: Edges::apply_ranges(edges, parent, right_edges, right_parent, apply),
|
||||||
|
},
|
||||||
|
(Edges::String { edges }, Edges::String { edges: right_edges }) => Edges::String {
|
||||||
|
edges: Edges::apply_ranges(edges, parent, right_edges, right_parent, apply),
|
||||||
|
},
|
||||||
|
// For boolean variables, we simply merge the low and high edges.
|
||||||
|
(
|
||||||
|
Edges::Boolean { high, low },
|
||||||
|
Edges::Boolean {
|
||||||
|
high: right_high,
|
||||||
|
low: right_low,
|
||||||
|
},
|
||||||
|
) => Edges::Boolean {
|
||||||
|
high: apply(high.negate(parent), right_high.negate(parent)),
|
||||||
|
low: apply(low.negate(parent), right_low.negate(parent)),
|
||||||
|
},
|
||||||
|
_ => unreachable!("cannot apply two `Edges` of different types"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge two range maps, applying the given operation to all disjoint, intersecting ranges.
|
||||||
|
///
|
||||||
|
/// For example, two nodes might have the following edges:
|
||||||
|
/// ```text
|
||||||
|
/// left (python_version): { [0, 3.4): A, [3.4, 3.4]: B, (3.4, inf): C }
|
||||||
|
/// right (python_version): { [0, 3.6): D, [3.6, 3.6]: E, (3.6, inf): F }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Unlike with boolean variables, we can't simply apply the operation the static `true`
|
||||||
|
/// and `false` edges. Instead, we have to split and merge overlapping ranges:
|
||||||
|
/// ```text
|
||||||
|
/// python_version: {
|
||||||
|
/// [0, 3.4): (A and D),
|
||||||
|
/// [3.4, 3.4]: (B and D),
|
||||||
|
/// (3.4, 3.6): (C and D),
|
||||||
|
/// [3.6, 3.6]: (C and E),
|
||||||
|
/// (3.6, inf): (C and F)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The left and right edges may also have a restricted range from calls to `restrict_versions`.
|
||||||
|
/// In that case, we drop any ranges that do not exist in the domain of both edges. Note that
|
||||||
|
/// this should not occur in practice because `requires-python` bounds are global.
|
||||||
|
fn apply_ranges<T>(
|
||||||
|
left_edges: &SmallVec<(Range<T>, NodeId)>,
|
||||||
|
left_parent: NodeId,
|
||||||
|
right_edges: &SmallVec<(Range<T>, NodeId)>,
|
||||||
|
right_parent: NodeId,
|
||||||
|
mut apply: impl FnMut(NodeId, NodeId) -> NodeId,
|
||||||
|
) -> SmallVec<(Range<T>, NodeId)>
|
||||||
|
where
|
||||||
|
T: Clone + Ord,
|
||||||
|
{
|
||||||
|
let mut combined = SmallVec::new();
|
||||||
|
for (left_range, left_child) in left_edges {
|
||||||
|
// Split the two maps into a set of disjoint and overlapping ranges, merging the
|
||||||
|
// intersections.
|
||||||
|
//
|
||||||
|
// Note that restrict ranges (see `restrict_versions`) makes finding intersections
|
||||||
|
// a bit more complicated despite the ranges being sorted. We cannot simply zip both
|
||||||
|
// sets, as they may contain arbitrary gaps. Instead, we use a quadratic search for
|
||||||
|
// simplicity as the set of ranges for a given variable is typically very small.
|
||||||
|
for (right_range, right_child) in right_edges {
|
||||||
|
let intersection = right_range.intersection(left_range);
|
||||||
|
if intersection.is_empty() {
|
||||||
|
// TODO(ibraheem): take advantage of the sorted ranges to `break` early
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the intersection.
|
||||||
|
let node = apply(
|
||||||
|
left_child.negate(left_parent),
|
||||||
|
right_child.negate(right_parent),
|
||||||
|
);
|
||||||
|
|
||||||
|
match combined.last_mut() {
|
||||||
|
// Combine ranges if possible.
|
||||||
|
Some((range, prev)) if *prev == node && can_conjoin(range, &intersection) => {
|
||||||
|
*range = range.union(&intersection);
|
||||||
|
}
|
||||||
|
_ => combined.push((intersection.clone(), node)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the given function to all direct children of this node.
|
||||||
|
fn map(&self, parent: NodeId, mut f: impl FnMut(NodeId) -> NodeId) -> Edges {
|
||||||
|
match self {
|
||||||
|
Edges::Version { edges: map } => Edges::Version {
|
||||||
|
edges: map
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|(range, node)| (range, f(node.negate(parent))))
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
Edges::String { edges: map } => Edges::String {
|
||||||
|
edges: map
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|(range, node)| (range, f(node.negate(parent))))
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
Edges::Boolean { high, low } => Edges::Boolean {
|
||||||
|
low: f(low.negate(parent)),
|
||||||
|
high: f(high.negate(parent)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an iterator over all direct children of this node.
|
||||||
|
fn nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
|
||||||
|
match self {
|
||||||
|
Edges::Version { edges: map } => {
|
||||||
|
Either::Left(Either::Left(map.iter().map(|(_, node)| *node)))
|
||||||
|
}
|
||||||
|
Edges::String { edges: map } => {
|
||||||
|
Either::Left(Either::Right(map.iter().map(|(_, node)| *node)))
|
||||||
|
}
|
||||||
|
Edges::Boolean { high, low } => Either::Right([*high, *low].into_iter()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the complement of this [`Edges`].
|
||||||
|
fn not(self) -> Edges {
|
||||||
|
match self {
|
||||||
|
Edges::Version { edges: map } => Edges::Version {
|
||||||
|
edges: map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(range, node)| (range, node.not()))
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
Edges::String { edges: map } => Edges::String {
|
||||||
|
edges: map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(range, node)| (range, node.not()))
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
Edges::Boolean { high, low } => Edges::Boolean {
|
||||||
|
high: high.not(),
|
||||||
|
low: low.not(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares the start of two ranges that are known to be disjoint.
|
||||||
|
fn compare_disjoint_range_start<T>(range1: &Range<T>, range2: &Range<T>) -> Ordering
|
||||||
|
where
|
||||||
|
T: Ord,
|
||||||
|
{
|
||||||
|
let (upper1, _) = range1.bounding_range().unwrap();
|
||||||
|
let (upper2, _) = range2.bounding_range().unwrap();
|
||||||
|
|
||||||
|
match (upper1, upper2) {
|
||||||
|
(Bound::Unbounded, _) => Ordering::Less,
|
||||||
|
(_, Bound::Unbounded) => Ordering::Greater,
|
||||||
|
(Bound::Included(v1), Bound::Excluded(v2)) if v1 == v2 => Ordering::Less,
|
||||||
|
(Bound::Excluded(v1), Bound::Included(v2)) if v1 == v2 => Ordering::Greater,
|
||||||
|
// Note that the ranges are disjoint, so their lower bounds cannot be equal.
|
||||||
|
(Bound::Included(v1) | Bound::Excluded(v1), Bound::Included(v2) | Bound::Excluded(v2)) => {
|
||||||
|
v1.cmp(v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if two disjoint ranges can be conjoined seamlessly without introducing a gap.
|
||||||
|
fn can_conjoin<T>(range1: &Range<T>, range2: &Range<T>) -> bool
|
||||||
|
where
|
||||||
|
T: Ord + Clone,
|
||||||
|
{
|
||||||
|
let Some((_, end)) = range1.bounding_range() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some((start, _)) = range2.bounding_range() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
match (end, start) {
|
||||||
|
(Bound::Included(v1), Bound::Excluded(v2)) if v1 == v2 => true,
|
||||||
|
(Bound::Excluded(v1), Bound::Included(v2)) if v1 == v2 => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for NodeId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
if *self == NodeId::FALSE {
|
||||||
|
return write!(f, "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
if *self == NodeId::TRUE {
|
||||||
|
return write!(f, "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_complement() {
|
||||||
|
write!(f, "{:?}", INTERNER.shared.node(*self).clone().not())
|
||||||
|
} else {
|
||||||
|
write!(f, "{:?}", INTERNER.shared.node(*self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{NodeId, INTERNER};
|
||||||
|
use crate::MarkerExpression;
|
||||||
|
|
||||||
|
fn expr(s: &str) -> NodeId {
|
||||||
|
INTERNER
|
||||||
|
.lock()
|
||||||
|
.expression(MarkerExpression::from_str(s).unwrap().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic() {
|
||||||
|
let m = || INTERNER.lock();
|
||||||
|
let extra_foo = expr("extra == 'foo'");
|
||||||
|
assert!(!extra_foo.is_false());
|
||||||
|
|
||||||
|
let os_foo = expr("os_name == 'foo'");
|
||||||
|
let extra_and_os_foo = m().or(extra_foo, os_foo);
|
||||||
|
assert!(!extra_and_os_foo.is_false());
|
||||||
|
assert!(!m().and(extra_foo, os_foo).is_false());
|
||||||
|
|
||||||
|
let trivially_true = m().or(extra_and_os_foo, extra_and_os_foo.not());
|
||||||
|
assert!(!trivially_true.is_false());
|
||||||
|
assert!(trivially_true.is_true());
|
||||||
|
|
||||||
|
let trivially_false = m().and(extra_foo, extra_foo.not());
|
||||||
|
assert!(trivially_false.is_false());
|
||||||
|
|
||||||
|
let e = m().or(trivially_false, os_foo);
|
||||||
|
assert!(!e.is_false());
|
||||||
|
|
||||||
|
let extra_not_foo = expr("extra != 'foo'");
|
||||||
|
assert!(m().and(extra_foo, extra_not_foo).is_false());
|
||||||
|
assert!(m().or(extra_foo, extra_not_foo).is_true());
|
||||||
|
|
||||||
|
let os_geq_bar = expr("os_name >= 'bar'");
|
||||||
|
assert!(!os_geq_bar.is_false());
|
||||||
|
|
||||||
|
let os_le_bar = expr("os_name < 'bar'");
|
||||||
|
assert!(m().and(os_geq_bar, os_le_bar).is_false());
|
||||||
|
assert!(m().or(os_geq_bar, os_le_bar).is_true());
|
||||||
|
|
||||||
|
let os_leq_bar = expr("os_name <= 'bar'");
|
||||||
|
assert!(!m().and(os_geq_bar, os_leq_bar).is_false());
|
||||||
|
assert!(m().or(os_geq_bar, os_leq_bar).is_true());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version() {
|
||||||
|
let m = || INTERNER.lock();
|
||||||
|
let eq_3 = expr("python_version == '3'");
|
||||||
|
let neq_3 = expr("python_version != '3'");
|
||||||
|
let geq_3 = expr("python_version >= '3'");
|
||||||
|
let leq_3 = expr("python_version <= '3'");
|
||||||
|
|
||||||
|
let eq_2 = expr("python_version == '2'");
|
||||||
|
let eq_1 = expr("python_version == '1'");
|
||||||
|
assert!(m().and(eq_2, eq_1).is_false());
|
||||||
|
|
||||||
|
assert_eq!(eq_3.not(), neq_3);
|
||||||
|
assert_eq!(eq_3, neq_3.not());
|
||||||
|
|
||||||
|
assert!(m().and(eq_3, neq_3).is_false());
|
||||||
|
assert!(m().or(eq_3, neq_3).is_true());
|
||||||
|
|
||||||
|
assert_eq!(m().and(eq_3, geq_3), eq_3);
|
||||||
|
assert_eq!(m().and(eq_3, leq_3), eq_3);
|
||||||
|
|
||||||
|
assert_eq!(m().and(geq_3, leq_3), eq_3);
|
||||||
|
|
||||||
|
assert!(!m().and(geq_3, leq_3).is_false());
|
||||||
|
assert!(m().or(geq_3, leq_3).is_true());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simplify() {
|
||||||
|
let m = || INTERNER.lock();
|
||||||
|
let x86 = expr("platform_machine == 'x86_64'");
|
||||||
|
let not_x86 = expr("platform_machine != 'x86_64'");
|
||||||
|
let windows = expr("platform_machine == 'Windows'");
|
||||||
|
|
||||||
|
let a = m().and(x86, windows);
|
||||||
|
let b = m().and(not_x86, windows);
|
||||||
|
assert_eq!(m().or(a, b), windows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,15 @@
|
||||||
//! outcomes. This implementation tries to carefully validate everything and emit warnings whenever
|
//! outcomes. This implementation tries to carefully validate everything and emit warnings whenever
|
||||||
//! bogus comparisons with unintended semantics are made.
|
//! bogus comparisons with unintended semantics are made.
|
||||||
|
|
||||||
|
mod algebra;
|
||||||
mod environment;
|
mod environment;
|
||||||
pub(crate) mod parse;
|
pub(crate) mod parse;
|
||||||
|
mod simplify;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
||||||
pub use environment::{MarkerEnvironment, MarkerEnvironmentBuilder};
|
pub use environment::{MarkerEnvironment, MarkerEnvironmentBuilder};
|
||||||
pub use tree::{
|
pub use tree::{
|
||||||
ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue, MarkerValueString,
|
ContainsMarkerTree, ExtraMarkerTree, ExtraOperator, InMarkerTree, MarkerExpression,
|
||||||
MarkerValueVersion, MarkerWarningKind, StringVersion,
|
MarkerOperator, MarkerTree, MarkerTreeContents, MarkerTreeKind, MarkerValue, MarkerValueString,
|
||||||
|
MarkerValueVersion, MarkerWarningKind, StringMarkerTree, StringVersion, VersionMarkerTree,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ pub(crate) fn parse_marker_value<T: Pep508Url>(
|
||||||
pub(crate) fn parse_marker_key_op_value<T: Pep508Url>(
|
pub(crate) fn parse_marker_key_op_value<T: Pep508Url>(
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
reporter: &mut impl Reporter,
|
reporter: &mut impl Reporter,
|
||||||
) -> Result<MarkerExpression, Pep508Error<T>> {
|
) -> Result<Option<MarkerExpression>, Pep508Error<T>> {
|
||||||
cursor.eat_whitespace();
|
cursor.eat_whitespace();
|
||||||
let l_value = parse_marker_value(cursor)?;
|
let l_value = parse_marker_value(cursor)?;
|
||||||
cursor.eat_whitespace();
|
cursor.eat_whitespace();
|
||||||
|
|
@ -139,25 +139,14 @@ pub(crate) fn parse_marker_key_op_value<T: Pep508Url>(
|
||||||
MarkerWarningKind::Pep440Error,
|
MarkerWarningKind::Pep440Error,
|
||||||
format!(
|
format!(
|
||||||
"Expected double quoted PEP 440 version to compare with {key},
|
"Expected double quoted PEP 440 version to compare with {key},
|
||||||
found {r_value}, will evaluate to false"
|
found {r_value}, will be ignored"
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Ok(MarkerExpression::arbitrary(
|
return Ok(None);
|
||||||
MarkerValue::MarkerEnvVersion(key),
|
|
||||||
operator,
|
|
||||||
r_value,
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match parse_version_expr(key.clone(), operator, &value, reporter) {
|
parse_version_expr(key.clone(), operator, &value, reporter)
|
||||||
Some(expr) => expr,
|
|
||||||
None => MarkerExpression::arbitrary(
|
|
||||||
MarkerValue::MarkerEnvVersion(key),
|
|
||||||
operator,
|
|
||||||
MarkerValue::QuotedString(value),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// The only sound choice for this is `<env key> <op> <string>`
|
// The only sound choice for this is `<env key> <op> <string>`
|
||||||
MarkerValue::MarkerEnvString(key) => {
|
MarkerValue::MarkerEnvString(key) => {
|
||||||
|
|
@ -168,24 +157,29 @@ pub(crate) fn parse_marker_key_op_value<T: Pep508Url>(
|
||||||
reporter.report(
|
reporter.report(
|
||||||
MarkerWarningKind::MarkerMarkerComparison,
|
MarkerWarningKind::MarkerMarkerComparison,
|
||||||
"Comparing two markers with each other doesn't make any sense,
|
"Comparing two markers with each other doesn't make any sense,
|
||||||
will evaluate to false"
|
will be ignored"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Ok(MarkerExpression::arbitrary(
|
return Ok(None);
|
||||||
MarkerValue::MarkerEnvString(key),
|
|
||||||
operator,
|
|
||||||
r_value,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
MarkerValue::QuotedString(r_string) => r_string,
|
MarkerValue::QuotedString(r_string) => r_string,
|
||||||
};
|
};
|
||||||
|
|
||||||
MarkerExpression::String {
|
if operator == MarkerOperator::TildeEqual {
|
||||||
|
reporter.report(
|
||||||
|
MarkerWarningKind::LexicographicComparison,
|
||||||
|
"Can't compare strings with `~=`, will be ignored".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(MarkerExpression::String {
|
||||||
key,
|
key,
|
||||||
operator,
|
operator,
|
||||||
value,
|
value,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
// `extra == '...'`
|
// `extra == '...'`
|
||||||
MarkerValue::Extra => {
|
MarkerValue::Extra => {
|
||||||
|
|
@ -196,73 +190,46 @@ pub(crate) fn parse_marker_key_op_value<T: Pep508Url>(
|
||||||
reporter.report(
|
reporter.report(
|
||||||
MarkerWarningKind::ExtraInvalidComparison,
|
MarkerWarningKind::ExtraInvalidComparison,
|
||||||
"Comparing extra with something other than a quoted string is wrong,
|
"Comparing extra with something other than a quoted string is wrong,
|
||||||
will evaluate to false"
|
will be ignored"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Ok(MarkerExpression::arbitrary(l_value, operator, r_value));
|
return Ok(None);
|
||||||
}
|
}
|
||||||
MarkerValue::QuotedString(value) => value,
|
MarkerValue::QuotedString(value) => value,
|
||||||
};
|
};
|
||||||
|
|
||||||
match parse_extra_expr(operator, &value, reporter) {
|
parse_extra_expr(operator, &value, reporter)
|
||||||
Some(expr) => expr,
|
|
||||||
None => MarkerExpression::arbitrary(
|
|
||||||
MarkerValue::Extra,
|
|
||||||
operator,
|
|
||||||
MarkerValue::QuotedString(value),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// This is either MarkerEnvVersion, MarkerEnvString or Extra inverted
|
// This is either MarkerEnvVersion, MarkerEnvString or Extra inverted
|
||||||
MarkerValue::QuotedString(l_string) => {
|
MarkerValue::QuotedString(l_string) => {
|
||||||
match r_value {
|
match r_value {
|
||||||
// The only sound choice for this is `<quoted PEP 440 version> <version op>` <version key>
|
// The only sound choice for this is `<quoted PEP 440 version> <version op>` <version key>
|
||||||
MarkerValue::MarkerEnvVersion(key) => {
|
MarkerValue::MarkerEnvVersion(key) => {
|
||||||
match parse_inverted_version_expr(&l_string, operator, key.clone(), reporter) {
|
parse_inverted_version_expr(&l_string, operator, key.clone(), reporter)
|
||||||
Some(expr) => expr,
|
|
||||||
None => MarkerExpression::arbitrary(
|
|
||||||
MarkerValue::QuotedString(l_string),
|
|
||||||
operator,
|
|
||||||
MarkerValue::MarkerEnvVersion(key),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// '...' == <env key>
|
// '...' == <env key>
|
||||||
MarkerValue::MarkerEnvString(key) => MarkerExpression::String {
|
MarkerValue::MarkerEnvString(key) => Some(MarkerExpression::String {
|
||||||
key,
|
key,
|
||||||
// Invert the operator to normalize the expression order.
|
// Invert the operator to normalize the expression order.
|
||||||
operator: operator.invert(),
|
operator: operator.invert(),
|
||||||
value: l_string,
|
value: l_string,
|
||||||
},
|
}),
|
||||||
// `'...' == extra`
|
// `'...' == extra`
|
||||||
MarkerValue::Extra => match parse_extra_expr(operator, &l_string, reporter) {
|
MarkerValue::Extra => parse_extra_expr(operator, &l_string, reporter),
|
||||||
Some(expr) => expr,
|
|
||||||
None => MarkerExpression::arbitrary(
|
|
||||||
MarkerValue::QuotedString(l_string),
|
|
||||||
operator,
|
|
||||||
MarkerValue::Extra,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// `'...' == '...'`, doesn't make much sense
|
// `'...' == '...'`, doesn't make much sense
|
||||||
MarkerValue::QuotedString(_) => {
|
MarkerValue::QuotedString(_) => {
|
||||||
// Not even pypa/packaging 22.0 supports this
|
// Not even pypa/packaging 22.0 supports this
|
||||||
// https://github.com/pypa/packaging/issues/632
|
// https://github.com/pypa/packaging/issues/632
|
||||||
let expr = MarkerExpression::arbitrary(
|
|
||||||
MarkerValue::QuotedString(l_string),
|
|
||||||
operator,
|
|
||||||
r_value,
|
|
||||||
);
|
|
||||||
|
|
||||||
reporter.report(
|
reporter.report(
|
||||||
MarkerWarningKind::StringStringComparison,
|
MarkerWarningKind::StringStringComparison,
|
||||||
format!(
|
format!(
|
||||||
"Comparing two quoted strings with each other doesn't make sense:
|
"Comparing two quoted strings with each other doesn't make sense:
|
||||||
{expr}, will evaluate to false"
|
'{l_string}' {operator} {r_value}, will be ignored"
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expr
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +254,7 @@ fn parse_version_expr(
|
||||||
MarkerWarningKind::Pep440Error,
|
MarkerWarningKind::Pep440Error,
|
||||||
format!(
|
format!(
|
||||||
"Expected PEP 440 version to compare with {key}, found {value},
|
"Expected PEP 440 version to compare with {key}, found {value},
|
||||||
will evaluate to false: {err}"
|
will be ignored: {err}"
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -300,7 +267,7 @@ fn parse_version_expr(
|
||||||
MarkerWarningKind::Pep440Error,
|
MarkerWarningKind::Pep440Error,
|
||||||
format!(
|
format!(
|
||||||
"Expected PEP 440 version operator to compare {key} with '{version}',
|
"Expected PEP 440 version operator to compare {key} with '{version}',
|
||||||
found '{marker_operator}', will evaluate to false",
|
found '{marker_operator}', will be ignored",
|
||||||
version = pattern.version()
|
version = pattern.version()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -342,7 +309,7 @@ fn parse_inverted_version_expr(
|
||||||
MarkerWarningKind::Pep440Error,
|
MarkerWarningKind::Pep440Error,
|
||||||
format!(
|
format!(
|
||||||
"Expected PEP 440 version to compare with {key}, found {value},
|
"Expected PEP 440 version to compare with {key}, found {value},
|
||||||
will evaluate to false: {err}"
|
will be ignored: {err}"
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -355,7 +322,7 @@ fn parse_inverted_version_expr(
|
||||||
MarkerWarningKind::Pep440Error,
|
MarkerWarningKind::Pep440Error,
|
||||||
format!(
|
format!(
|
||||||
"Expected PEP 440 version operator to compare {key} with '{version}',
|
"Expected PEP 440 version operator to compare {key} with '{version}',
|
||||||
found '{marker_operator}', will evaluate to false"
|
found '{marker_operator}', will be ignored"
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -388,7 +355,7 @@ fn parse_extra_expr(
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
reporter.report(
|
reporter.report(
|
||||||
MarkerWarningKind::ExtraInvalidComparison,
|
MarkerWarningKind::ExtraInvalidComparison,
|
||||||
format!("Expected extra name, found '{value}', will evaluate to false: {err}"),
|
format!("Expected extra name, found '{value}', will be ignored: {err}"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -402,9 +369,10 @@ fn parse_extra_expr(
|
||||||
reporter.report(
|
reporter.report(
|
||||||
MarkerWarningKind::ExtraInvalidComparison,
|
MarkerWarningKind::ExtraInvalidComparison,
|
||||||
"Comparing extra with something other than a quoted string is wrong,
|
"Comparing extra with something other than a quoted string is wrong,
|
||||||
will evaluate to false"
|
will be ignored"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,16 +383,14 @@ fn parse_extra_expr(
|
||||||
fn parse_marker_expr<T: Pep508Url>(
|
fn parse_marker_expr<T: Pep508Url>(
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
reporter: &mut impl Reporter,
|
reporter: &mut impl Reporter,
|
||||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
|
||||||
cursor.eat_whitespace();
|
cursor.eat_whitespace();
|
||||||
if let Some(start_pos) = cursor.eat_char('(') {
|
if let Some(start_pos) = cursor.eat_char('(') {
|
||||||
let marker = parse_marker_or(cursor, reporter)?;
|
let marker = parse_marker_or(cursor, reporter)?;
|
||||||
cursor.next_expect_char(')', start_pos)?;
|
cursor.next_expect_char(')', start_pos)?;
|
||||||
Ok(marker)
|
Ok(marker)
|
||||||
} else {
|
} else {
|
||||||
Ok(MarkerTree::Expression(parse_marker_key_op_value(
|
Ok(parse_marker_key_op_value(cursor, reporter)?.map(MarkerTree::expression))
|
||||||
cursor, reporter,
|
|
||||||
)?))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,8 +401,8 @@ fn parse_marker_expr<T: Pep508Url>(
|
||||||
fn parse_marker_and<T: Pep508Url>(
|
fn parse_marker_and<T: Pep508Url>(
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
reporter: &mut impl Reporter,
|
reporter: &mut impl Reporter,
|
||||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
|
||||||
parse_marker_op(cursor, "and", MarkerTree::And, parse_marker_expr, reporter)
|
parse_marker_op(cursor, "and", MarkerTree::and, parse_marker_expr, reporter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ```text
|
/// ```text
|
||||||
|
|
@ -446,29 +412,37 @@ fn parse_marker_and<T: Pep508Url>(
|
||||||
fn parse_marker_or<T: Pep508Url>(
|
fn parse_marker_or<T: Pep508Url>(
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
reporter: &mut impl Reporter,
|
reporter: &mut impl Reporter,
|
||||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
|
||||||
parse_marker_op(cursor, "or", MarkerTree::Or, parse_marker_and, reporter)
|
parse_marker_op(
|
||||||
|
cursor,
|
||||||
|
"or",
|
||||||
|
MarkerTree::or,
|
||||||
|
|cursor, reporter| parse_marker_and(cursor, reporter),
|
||||||
|
reporter,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses both `marker_and` and `marker_or`
|
/// Parses both `marker_and` and `marker_or`
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
fn parse_marker_op<T: Pep508Url, R: Reporter>(
|
fn parse_marker_op<T: Pep508Url, R: Reporter>(
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
op: &str,
|
op: &str,
|
||||||
op_constructor: fn(Vec<MarkerTree>) -> MarkerTree,
|
apply: fn(&mut MarkerTree, MarkerTree),
|
||||||
parse_inner: fn(&mut Cursor, &mut R) -> Result<MarkerTree, Pep508Error<T>>,
|
parse_inner: fn(&mut Cursor, &mut R) -> Result<Option<MarkerTree>, Pep508Error<T>>,
|
||||||
reporter: &mut R,
|
reporter: &mut R,
|
||||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
|
||||||
|
let mut tree = None;
|
||||||
|
|
||||||
// marker_and or marker_expr
|
// marker_and or marker_expr
|
||||||
let first_element = parse_inner(cursor, reporter)?;
|
let first_element = parse_inner(cursor, reporter)?;
|
||||||
// wsp*
|
|
||||||
cursor.eat_whitespace();
|
if let Some(expression) = first_element {
|
||||||
// Check if we're done here instead of invoking the whole vec allocating loop
|
match tree {
|
||||||
if matches!(cursor.peek_char(), None | Some(')')) {
|
Some(ref mut tree) => apply(tree, expression),
|
||||||
return Ok(first_element);
|
None => tree = Some(expression),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut expressions = Vec::with_capacity(1);
|
|
||||||
expressions.push(first_element);
|
|
||||||
loop {
|
loop {
|
||||||
// wsp*
|
// wsp*
|
||||||
cursor.eat_whitespace();
|
cursor.eat_whitespace();
|
||||||
|
|
@ -477,17 +451,15 @@ fn parse_marker_op<T: Pep508Url, R: Reporter>(
|
||||||
match cursor.slice(start, len) {
|
match cursor.slice(start, len) {
|
||||||
value if value == op => {
|
value if value == op => {
|
||||||
cursor.take_while(|c| !c.is_whitespace());
|
cursor.take_while(|c| !c.is_whitespace());
|
||||||
let expression = parse_inner(cursor, reporter)?;
|
|
||||||
expressions.push(expression);
|
if let Some(expression) = parse_inner(cursor, reporter)? {
|
||||||
}
|
match tree {
|
||||||
_ => {
|
Some(ref mut tree) => apply(tree, expression),
|
||||||
// Build minimal trees
|
None => tree = Some(expression),
|
||||||
return if expressions.len() == 1 {
|
}
|
||||||
Ok(expressions.remove(0))
|
}
|
||||||
} else {
|
|
||||||
Ok(op_constructor(expressions))
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
_ => return Ok(tree),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -498,7 +470,7 @@ fn parse_marker_op<T: Pep508Url, R: Reporter>(
|
||||||
pub(crate) fn parse_markers_cursor<T: Pep508Url>(
|
pub(crate) fn parse_markers_cursor<T: Pep508Url>(
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
reporter: &mut impl Reporter,
|
reporter: &mut impl Reporter,
|
||||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
|
||||||
let marker = parse_marker_or(cursor, reporter)?;
|
let marker = parse_marker_or(cursor, reporter)?;
|
||||||
cursor.eat_whitespace();
|
cursor.eat_whitespace();
|
||||||
if let Some((pos, unexpected)) = cursor.next() {
|
if let Some((pos, unexpected)) = cursor.next() {
|
||||||
|
|
@ -513,6 +485,7 @@ pub(crate) fn parse_markers_cursor<T: Pep508Url>(
|
||||||
input: cursor.to_string(),
|
input: cursor.to_string(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(marker)
|
Ok(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -523,5 +496,8 @@ pub(crate) fn parse_markers<T: Pep508Url>(
|
||||||
reporter: &mut impl Reporter,
|
reporter: &mut impl Reporter,
|
||||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
) -> Result<MarkerTree, Pep508Error<T>> {
|
||||||
let mut chars = Cursor::new(markers);
|
let mut chars = Cursor::new(markers);
|
||||||
parse_markers_cursor(&mut chars, reporter)
|
|
||||||
|
// If the tree consisted entirely of arbitrary expressions
|
||||||
|
// that were ignored, it evaluates to true.
|
||||||
|
parse_markers_cursor(&mut chars, reporter).map(|result| result.unwrap_or(MarkerTree::TRUE))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
382
crates/pep508-rs/src/marker/simplify.rs
Normal file
382
crates/pep508-rs/src/marker/simplify.rs
Normal file
|
|
@ -0,0 +1,382 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Bound;
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use pep440_rs::VersionSpecifier;
|
||||||
|
use pubgrub::Range;
|
||||||
|
use rustc_hash::FxBuildHasher;
|
||||||
|
|
||||||
|
use crate::{ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerTreeKind};
|
||||||
|
|
||||||
|
/// Returns a simplified DNF expression for a given marker tree.
|
||||||
|
///
|
||||||
|
/// Marker trees are represented as decision diagrams that cannot be directly serialized to.
|
||||||
|
/// a boolean expression. Instead, you must traverse and collect all possible solutions to the
|
||||||
|
/// diagram, which can be used to create a DNF expression, or all non-solutions to the diagram,
|
||||||
|
/// which can be used to create a CNF expression.
|
||||||
|
///
|
||||||
|
/// We choose DNF as it is easier to simplify for user-facing output.
|
||||||
|
pub(crate) fn to_dnf(tree: &MarkerTree) -> Vec<Vec<MarkerExpression>> {
|
||||||
|
let mut dnf = Vec::new();
|
||||||
|
collect_dnf(tree, &mut dnf, &mut Vec::new());
|
||||||
|
simplify(&mut dnf);
|
||||||
|
dnf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk a [`MarkerTree`] recursively and construct a DNF expression.
|
||||||
|
///
|
||||||
|
/// A decision diagram can be converted to DNF form by performing a depth-first traversal of
|
||||||
|
/// the tree and collecting all paths to a `true` terminal node.
|
||||||
|
///
|
||||||
|
/// `path` is the list of marker expressions traversed on the current path.
|
||||||
|
fn collect_dnf(
|
||||||
|
tree: &MarkerTree,
|
||||||
|
dnf: &mut Vec<Vec<MarkerExpression>>,
|
||||||
|
path: &mut Vec<MarkerExpression>,
|
||||||
|
) {
|
||||||
|
match tree.kind() {
|
||||||
|
// Reached a `false` node, meaning the conjunction is irrelevant for DNF.
|
||||||
|
MarkerTreeKind::False => {}
|
||||||
|
// Reached a solution, store the conjunction.
|
||||||
|
MarkerTreeKind::True => {
|
||||||
|
if !path.is_empty() {
|
||||||
|
dnf.push(path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MarkerTreeKind::Version(marker) => {
|
||||||
|
for (tree, range) in collect_edges(marker.edges()) {
|
||||||
|
// Detect whether the range for this edge can be simplified as an inequality.
|
||||||
|
if let Some(excluded) = range_inequality(&range) {
|
||||||
|
let current = path.len();
|
||||||
|
for version in excluded {
|
||||||
|
path.push(MarkerExpression::Version {
|
||||||
|
key: marker.key().clone(),
|
||||||
|
specifier: VersionSpecifier::not_equals_version(version.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_dnf(&tree, dnf, path);
|
||||||
|
path.truncate(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for bounds in range.iter() {
|
||||||
|
let current = path.len();
|
||||||
|
for specifier in VersionSpecifier::from_bounds(bounds) {
|
||||||
|
path.push(MarkerExpression::Version {
|
||||||
|
key: marker.key().clone(),
|
||||||
|
specifier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_dnf(&tree, dnf, path);
|
||||||
|
path.truncate(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MarkerTreeKind::String(marker) => {
|
||||||
|
for (tree, range) in collect_edges(marker.children()) {
|
||||||
|
// Detect whether the range for this edge can be simplified as an inequality.
|
||||||
|
if let Some(excluded) = range_inequality(&range) {
|
||||||
|
let current = path.len();
|
||||||
|
for value in excluded {
|
||||||
|
path.push(MarkerExpression::String {
|
||||||
|
key: marker.key().clone(),
|
||||||
|
operator: MarkerOperator::NotEqual,
|
||||||
|
value: value.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_dnf(&tree, dnf, path);
|
||||||
|
path.truncate(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for bounds in range.iter() {
|
||||||
|
let current = path.len();
|
||||||
|
for (operator, value) in MarkerOperator::from_bounds(bounds) {
|
||||||
|
path.push(MarkerExpression::String {
|
||||||
|
key: marker.key().clone(),
|
||||||
|
operator,
|
||||||
|
value: value.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_dnf(&tree, dnf, path);
|
||||||
|
path.truncate(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MarkerTreeKind::In(marker) => {
|
||||||
|
for (value, tree) in marker.children() {
|
||||||
|
let operator = if value {
|
||||||
|
MarkerOperator::In
|
||||||
|
} else {
|
||||||
|
MarkerOperator::NotIn
|
||||||
|
};
|
||||||
|
|
||||||
|
let expr = MarkerExpression::String {
|
||||||
|
key: marker.key().clone(),
|
||||||
|
value: marker.value().to_owned(),
|
||||||
|
operator,
|
||||||
|
};
|
||||||
|
|
||||||
|
path.push(expr);
|
||||||
|
collect_dnf(&tree, dnf, path);
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MarkerTreeKind::Contains(marker) => {
|
||||||
|
for (value, tree) in marker.children() {
|
||||||
|
let operator = if value {
|
||||||
|
MarkerOperator::Contains
|
||||||
|
} else {
|
||||||
|
MarkerOperator::NotContains
|
||||||
|
};
|
||||||
|
|
||||||
|
let expr = MarkerExpression::String {
|
||||||
|
key: marker.key().clone(),
|
||||||
|
value: marker.value().to_owned(),
|
||||||
|
operator,
|
||||||
|
};
|
||||||
|
|
||||||
|
path.push(expr);
|
||||||
|
collect_dnf(&tree, dnf, path);
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MarkerTreeKind::Extra(marker) => {
|
||||||
|
for (value, tree) in marker.children() {
|
||||||
|
let operator = if value {
|
||||||
|
ExtraOperator::Equal
|
||||||
|
} else {
|
||||||
|
ExtraOperator::NotEqual
|
||||||
|
};
|
||||||
|
|
||||||
|
let expr = MarkerExpression::Extra {
|
||||||
|
name: marker.name().clone(),
|
||||||
|
operator,
|
||||||
|
};
|
||||||
|
|
||||||
|
path.push(expr);
|
||||||
|
collect_dnf(&tree, dnf, path);
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simplifies a DNF expression.
|
||||||
|
///
|
||||||
|
/// A decision diagram is canonical, but only for a given variable order. Depending on the
|
||||||
|
/// pre-defined order, the DNF expression produced by a decision tree can still be further
|
||||||
|
/// simplified.
|
||||||
|
///
|
||||||
|
/// For example, the decision diagram for the expression `A or B` will be represented as
|
||||||
|
/// `A or (not A and B)` or `B or (not B and A)`, depending on the variable order. In both
|
||||||
|
/// cases, the negation in the second clause is redundant.
|
||||||
|
///
|
||||||
|
/// Completely simplifying a DNF expression is NP-hard and amounts to the set cover problem.
|
||||||
|
/// Additionally, marker expressions can contain complex expressions involving version ranges
|
||||||
|
/// that are not trivial to simplify. Instead, we choose to simplify at the boolean variable
|
||||||
|
/// level without any truth table expansion. Combined with the normalization applied by decision
|
||||||
|
/// trees, this seems to be sufficient in practice.
|
||||||
|
///
|
||||||
|
/// Note: This function has quadratic time complexity. However, it is not applied on every marker
|
||||||
|
/// operation, only to user facing output, which are typically very simple.
|
||||||
|
fn simplify(dnf: &mut Vec<Vec<MarkerExpression>>) {
|
||||||
|
for i in 0..dnf.len() {
|
||||||
|
let clause = &dnf[i];
|
||||||
|
|
||||||
|
// Find redundant terms in this clause.
|
||||||
|
let mut redundant_terms = Vec::new();
|
||||||
|
'term: for (skipped, skipped_term) in clause.iter().enumerate() {
|
||||||
|
for (j, other_clause) in dnf.iter().enumerate() {
|
||||||
|
if i == j {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let X be this clause with a given term A set to it's negation.
|
||||||
|
// If there exists another clause that is a subset of X, the term A is
|
||||||
|
// redundant in this clause.
|
||||||
|
//
|
||||||
|
// For example, `A or (not A and B)` can be simplified to `A or B`,
|
||||||
|
// eliminating the `not A` term.
|
||||||
|
if other_clause.iter().all(|term| {
|
||||||
|
// For the term to be redundant in this clause, the other clause can
|
||||||
|
// contain the negation of the term but not the term itself.
|
||||||
|
if term == skipped_term {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if is_negation(term, skipped_term) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(ibraheem): if we intern variables we could reduce this
|
||||||
|
// from a linear search to an integer `HashSet` lookup
|
||||||
|
clause
|
||||||
|
.iter()
|
||||||
|
.position(|x| x == term)
|
||||||
|
// If the term was already removed from this one, we cannot
|
||||||
|
// depend on it for further simplification.
|
||||||
|
.is_some_and(|i| !redundant_terms.contains(&i))
|
||||||
|
}) {
|
||||||
|
redundant_terms.push(skipped);
|
||||||
|
continue 'term;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminate any redundant terms.
|
||||||
|
redundant_terms.sort_by(|a, b| b.cmp(a));
|
||||||
|
for term in redundant_terms {
|
||||||
|
dnf[i].remove(term);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we have eliminated redundant terms, there may also be redundant clauses.
|
||||||
|
// For example, `(A and B) or (not A and B)` would have been simplified above to
|
||||||
|
// `(A and B) or B` and can now be further simplified to just `B`.
|
||||||
|
let mut redundant_clauses = Vec::new();
|
||||||
|
'clause: for i in 0..dnf.len() {
|
||||||
|
let clause = &dnf[i];
|
||||||
|
|
||||||
|
for (j, other_clause) in dnf.iter().enumerate() {
|
||||||
|
// Ignore clauses that are going to be eliminated.
|
||||||
|
if i == j || redundant_clauses.contains(&j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is another clause that is a subset of this one, thus this clause is redundant.
|
||||||
|
if other_clause.iter().all(|term| {
|
||||||
|
// TODO(ibraheem): if we intern variables we could reduce this
|
||||||
|
// from a linear search to an integer `HashSet` lookup
|
||||||
|
clause.contains(term)
|
||||||
|
}) {
|
||||||
|
redundant_clauses.push(i);
|
||||||
|
continue 'clause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminate any redundant clauses.
|
||||||
|
for i in redundant_clauses.into_iter().rev() {
|
||||||
|
dnf.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge any edges that lead to identical subtrees into a single range.
|
||||||
|
fn collect_edges<'a, T>(
|
||||||
|
map: impl ExactSizeIterator<Item = (&'a Range<T>, MarkerTree)>,
|
||||||
|
) -> IndexMap<MarkerTree, Range<T>, FxBuildHasher>
|
||||||
|
where
|
||||||
|
T: Ord + Clone + 'a,
|
||||||
|
{
|
||||||
|
let len = map.len();
|
||||||
|
|
||||||
|
let mut paths: IndexMap<_, Range<_>, FxBuildHasher> = IndexMap::default();
|
||||||
|
for (i, (range, tree)) in map.enumerate() {
|
||||||
|
let (mut start, mut end) = range.bounding_range().unwrap();
|
||||||
|
match (start, end) {
|
||||||
|
(Bound::Included(v1), Bound::Included(v2)) if v1 == v2 => {}
|
||||||
|
_ => {
|
||||||
|
// Expand the range of this variable to be unbounded.
|
||||||
|
// This helps remove redundant expressions such as `python_version >= '3.7'`
|
||||||
|
// when the range has already been restricted to `['3.7', inf)`
|
||||||
|
if i == 0 {
|
||||||
|
start = Bound::Unbounded;
|
||||||
|
}
|
||||||
|
if i == len - 1 {
|
||||||
|
end = Bound::Unbounded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine the ranges.
|
||||||
|
let range = Range::from_range_bounds((start.cloned(), end.cloned()));
|
||||||
|
paths
|
||||||
|
.entry(tree)
|
||||||
|
.and_modify(|union| *union = union.union(&range))
|
||||||
|
.or_insert_with(|| range.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Some` if the expression can be simplified as an inequality consisting
|
||||||
|
/// of the given values.
|
||||||
|
///
|
||||||
|
/// For example, `os_name < 'Linux'` and `os_name > 'Linux'` can be simplified to
|
||||||
|
/// `os_name != 'Linux'`.
|
||||||
|
fn range_inequality<T>(range: &Range<T>) -> Option<Vec<&T>>
|
||||||
|
where
|
||||||
|
T: Ord + Clone + fmt::Debug,
|
||||||
|
{
|
||||||
|
if range.is_empty() || range.bounding_range() != Some((Bound::Unbounded, Bound::Unbounded)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut excluded = Vec::new();
|
||||||
|
for ((_, end), (start, _)) in range.iter().tuple_windows() {
|
||||||
|
match (end, start) {
|
||||||
|
(Bound::Excluded(v1), Bound::Excluded(v2)) if v1 == v2 => excluded.push(v1),
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(excluded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the LHS is the negation of the RHS, or vice versa.
|
||||||
|
fn is_negation(left: &MarkerExpression, right: &MarkerExpression) -> bool {
|
||||||
|
match left {
|
||||||
|
MarkerExpression::Version { key, specifier } => {
|
||||||
|
let MarkerExpression::Version {
|
||||||
|
key: key2,
|
||||||
|
specifier: specifier2,
|
||||||
|
} = right
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
key == key2
|
||||||
|
&& specifier.version() == specifier2.version()
|
||||||
|
&& specifier
|
||||||
|
.operator()
|
||||||
|
.negate()
|
||||||
|
.is_some_and(|negated| negated == *specifier2.operator())
|
||||||
|
}
|
||||||
|
MarkerExpression::String {
|
||||||
|
key,
|
||||||
|
operator,
|
||||||
|
value,
|
||||||
|
} => {
|
||||||
|
let MarkerExpression::String {
|
||||||
|
key: key2,
|
||||||
|
operator: operator2,
|
||||||
|
value: value2,
|
||||||
|
} = right
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
key == key2
|
||||||
|
&& value == value2
|
||||||
|
&& operator
|
||||||
|
.negate()
|
||||||
|
.is_some_and(|negated| negated == *operator2)
|
||||||
|
}
|
||||||
|
MarkerExpression::Extra { operator, name } => {
|
||||||
|
let MarkerExpression::Extra {
|
||||||
|
name: name2,
|
||||||
|
operator: operator2,
|
||||||
|
} = right
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
name == name2 && operator.negate() == *operator2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -136,7 +136,7 @@ impl<Url: UnnamedRequirementUrl> Display for UnnamedRequirement<Url> {
|
||||||
.join(",")
|
.join(",")
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
if let Some(marker) = &self.marker {
|
if let Some(marker) = self.marker.as_ref().and_then(MarkerTree::contents) {
|
||||||
write!(f, " ; {marker}")?;
|
write!(f, " ; {marker}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -172,7 +172,7 @@ fn parse_unnamed_requirement<Url: UnnamedRequirementUrl>(
|
||||||
let marker = if cursor.peek_char() == Some(';') {
|
let marker = if cursor.peek_char() == Some(';') {
|
||||||
// Skip past the semicolon
|
// Skip past the semicolon
|
||||||
cursor.next();
|
cursor.next();
|
||||||
Some(parse::parse_markers_cursor(cursor, reporter)?)
|
parse::parse_markers_cursor(cursor, reporter)?
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use distribution_filename::DistExtension;
|
use distribution_filename::DistExtension;
|
||||||
use pep440_rs::VersionSpecifiers;
|
use serde::Serialize;
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use pep440_rs::VersionSpecifiers;
|
||||||
|
use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
|
||||||
use uv_fs::PortablePathBuf;
|
use uv_fs::PortablePathBuf;
|
||||||
use uv_git::{GitReference, GitSha, GitUrl};
|
use uv_git::{GitReference, GitSha, GitUrl};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
@ -37,6 +39,11 @@ pub struct Requirement {
|
||||||
pub name: PackageName,
|
pub name: PackageName,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||||
pub extras: Vec<ExtraName>,
|
pub extras: Vec<ExtraName>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "marker_is_empty",
|
||||||
|
serialize_with = "serialize_marker",
|
||||||
|
default
|
||||||
|
)]
|
||||||
pub marker: Option<MarkerTree>,
|
pub marker: Option<MarkerTree>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub source: RequirementSource,
|
pub source: RequirementSource,
|
||||||
|
|
@ -44,6 +51,17 @@ pub struct Requirement {
|
||||||
pub origin: Option<RequirementOrigin>,
|
pub origin: Option<RequirementOrigin>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn marker_is_empty(marker: &Option<MarkerTree>) -> bool {
|
||||||
|
marker.as_ref().and_then(MarkerTree::contents).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_marker<S>(marker: &Option<MarkerTree>, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
marker.as_ref().unwrap().contents().unwrap().serialize(s)
|
||||||
|
}
|
||||||
|
|
||||||
impl Requirement {
|
impl Requirement {
|
||||||
/// Returns whether the markers apply for the given environment.
|
/// Returns whether the markers apply for the given environment.
|
||||||
///
|
///
|
||||||
|
|
@ -238,7 +256,7 @@ impl Display for Requirement {
|
||||||
write!(f, " @ {url}")?;
|
write!(f, " @ {url}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(marker) = &self.marker {
|
if let Some(marker) = self.marker.as_ref().and_then(MarkerTree::contents) {
|
||||||
write!(f, " ; {marker}")?;
|
write!(f, " ; {marker}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -24,28 +24,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -78,28 +57,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -132,35 +90,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0' and platform_system == 'Windows',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: PlatformSystem,
|
|
||||||
operator: Equal,
|
|
||||||
value: "Windows",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -193,28 +123,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -248,28 +157,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
|
||||||
|
|
@ -24,28 +24,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -78,28 +57,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -132,35 +90,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0' and platform_system == 'Windows',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: PlatformSystem,
|
|
||||||
operator: Equal,
|
|
||||||
value: "Windows",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -193,28 +123,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -248,28 +157,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.8' and python_version < '4.0',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: LessThan,
|
|
||||||
version: "4.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
|
||||||
|
|
@ -168,26 +168,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.9' and os_name == 'posix',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: OsName,
|
|
||||||
operator: Equal,
|
|
||||||
value: "posix",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -246,26 +227,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.9' and os_name == 'posix',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: OsName,
|
|
||||||
operator: Equal,
|
|
||||||
value: "posix",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -317,26 +279,7 @@ RequirementsTxt {
|
||||||
},
|
},
|
||||||
extras: [],
|
extras: [],
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.9' and os_name == 'posix',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: OsName,
|
|
||||||
operator: Equal,
|
|
||||||
value: "posix",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
|
||||||
|
|
@ -168,26 +168,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.9' and os_name == 'posix',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: OsName,
|
|
||||||
operator: Equal,
|
|
||||||
value: "posix",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -246,26 +227,7 @@ RequirementsTxt {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.9' and os_name == 'posix',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: OsName,
|
|
||||||
operator: Equal,
|
|
||||||
value: "posix",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
@ -317,26 +279,7 @@ RequirementsTxt {
|
||||||
},
|
},
|
||||||
extras: [],
|
extras: [],
|
||||||
marker: Some(
|
marker: Some(
|
||||||
And(
|
python_version >= '3.9' and os_name == 'posix',
|
||||||
[
|
|
||||||
Expression(
|
|
||||||
Version {
|
|
||||||
key: PythonVersion,
|
|
||||||
specifier: VersionSpecifier {
|
|
||||||
operator: GreaterThanEqual,
|
|
||||||
version: "3.9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expression(
|
|
||||||
String {
|
|
||||||
key: OsName,
|
|
||||||
operator: Equal,
|
|
||||||
value: "posix",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
origin: Some(
|
origin: Some(
|
||||||
File(
|
File(
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,7 @@ impl Constraints {
|
||||||
let Some(extra_expression) = requirement
|
let Some(extra_expression) = requirement
|
||||||
.marker
|
.marker
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|marker| marker.top_level_extra())
|
.and_then(MarkerTree::top_level_extra)
|
||||||
.cloned()
|
|
||||||
else {
|
else {
|
||||||
// Case 2: A non-optional dependency with constraint(s).
|
// Case 2: A non-optional dependency with constraint(s).
|
||||||
return Either::Right(Either::Right(
|
return Either::Right(Either::Right(
|
||||||
|
|
@ -79,7 +78,7 @@ impl Constraints {
|
||||||
Either::Right(Either::Left(std::iter::once(requirement).chain(
|
Either::Right(Either::Left(std::iter::once(requirement).chain(
|
||||||
constraints.iter().cloned().map(move |constraint| {
|
constraints.iter().cloned().map(move |constraint| {
|
||||||
// Add the extra to the override marker.
|
// Add the extra to the override marker.
|
||||||
let mut joint_marker = MarkerTree::Expression(extra_expression.clone());
|
let mut joint_marker = MarkerTree::expression(extra_expression.clone());
|
||||||
if let Some(marker) = &constraint.marker {
|
if let Some(marker) = &constraint.marker {
|
||||||
joint_marker.and(marker.clone());
|
joint_marker.and(marker.clone());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ impl Overrides {
|
||||||
let Some(extra_expression) = requirement
|
let Some(extra_expression) = requirement
|
||||||
.marker
|
.marker
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|marker| marker.top_level_extra())
|
.and_then(MarkerTree::top_level_extra)
|
||||||
else {
|
else {
|
||||||
// Case 2: A non-optional dependency with override(s).
|
// Case 2: A non-optional dependency with override(s).
|
||||||
return Either::Right(Either::Right(overrides.iter().map(Cow::Borrowed)));
|
return Either::Right(Either::Right(overrides.iter().map(Cow::Borrowed)));
|
||||||
|
|
@ -63,17 +63,19 @@ impl Overrides {
|
||||||
//
|
//
|
||||||
// When the original requirement is an optional dependency, the override(s) need to
|
// When the original requirement is an optional dependency, the override(s) need to
|
||||||
// be optional for the same extra, otherwise we activate extras that should be inactive.
|
// be optional for the same extra, otherwise we activate extras that should be inactive.
|
||||||
Either::Right(Either::Left(overrides.iter().map(|override_requirement| {
|
Either::Right(Either::Left(overrides.iter().map(
|
||||||
// Add the extra to the override marker.
|
move |override_requirement| {
|
||||||
let mut joint_marker = MarkerTree::Expression(extra_expression.clone());
|
// Add the extra to the override marker.
|
||||||
if let Some(marker) = &override_requirement.marker {
|
let mut joint_marker = MarkerTree::expression(extra_expression.clone());
|
||||||
joint_marker.and(marker.clone());
|
if let Some(marker) = &override_requirement.marker {
|
||||||
}
|
joint_marker.and(marker.clone());
|
||||||
Cow::Owned(Requirement {
|
}
|
||||||
marker: Some(joint_marker.clone()),
|
Cow::Owned(Requirement {
|
||||||
..override_requirement.clone()
|
marker: Some(joint_marker.clone()),
|
||||||
})
|
..override_requirement.clone()
|
||||||
})))
|
})
|
||||||
|
},
|
||||||
|
)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
crates/uv-pubgrub/Cargo.toml
Normal file
15
crates/uv-pubgrub/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "uv-pubgrub"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Common uv pubgrub types."
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pep440_rs = { workspace = true }
|
||||||
|
|
||||||
|
itertools = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
pubgrub = { workspace = true }
|
||||||
|
|
@ -17,7 +17,7 @@ pub enum PubGrubSpecifierError {
|
||||||
pub struct PubGrubSpecifier(Range<Version>);
|
pub struct PubGrubSpecifier(Range<Version>);
|
||||||
|
|
||||||
impl PubGrubSpecifier {
|
impl PubGrubSpecifier {
|
||||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (&Bound<Version>, &Bound<Version>)> {
|
pub fn iter(&self) -> impl Iterator<Item = (&Bound<Version>, &Bound<Version>)> {
|
||||||
self.0.iter()
|
self.0.iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ impl From<PubGrubSpecifier> for Range<Version> {
|
||||||
impl PubGrubSpecifier {
|
impl PubGrubSpecifier {
|
||||||
/// Convert [`VersionSpecifiers`] to a PubGrub-compatible version range, using PEP 440
|
/// Convert [`VersionSpecifiers`] to a PubGrub-compatible version range, using PEP 440
|
||||||
/// semantics.
|
/// semantics.
|
||||||
pub(crate) fn from_pep440_specifiers(
|
pub fn from_pep440_specifiers(
|
||||||
specifiers: &VersionSpecifiers,
|
specifiers: &VersionSpecifiers,
|
||||||
) -> Result<Self, PubGrubSpecifierError> {
|
) -> Result<Self, PubGrubSpecifierError> {
|
||||||
let range = specifiers
|
let range = specifiers
|
||||||
|
|
@ -52,7 +52,7 @@ impl PubGrubSpecifier {
|
||||||
|
|
||||||
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using PEP 440
|
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using PEP 440
|
||||||
/// semantics.
|
/// semantics.
|
||||||
pub(crate) fn from_pep440_specifier(
|
pub fn from_pep440_specifier(
|
||||||
specifier: &VersionSpecifier,
|
specifier: &VersionSpecifier,
|
||||||
) -> Result<Self, PubGrubSpecifierError> {
|
) -> Result<Self, PubGrubSpecifierError> {
|
||||||
let ranges = match specifier.operator() {
|
let ranges = match specifier.operator() {
|
||||||
|
|
@ -159,7 +159,7 @@ impl PubGrubSpecifier {
|
||||||
/// is allowed for projects that declare `requires-python = ">3.13"`.
|
/// is allowed for projects that declare `requires-python = ">3.13"`.
|
||||||
///
|
///
|
||||||
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
|
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
|
||||||
pub(crate) fn from_release_specifiers(
|
pub fn from_release_specifiers(
|
||||||
specifiers: &VersionSpecifiers,
|
specifiers: &VersionSpecifiers,
|
||||||
) -> Result<Self, PubGrubSpecifierError> {
|
) -> Result<Self, PubGrubSpecifierError> {
|
||||||
let range = specifiers
|
let range = specifiers
|
||||||
|
|
@ -182,7 +182,7 @@ impl PubGrubSpecifier {
|
||||||
/// is allowed for projects that declare `requires-python = ">3.13"`.
|
/// is allowed for projects that declare `requires-python = ">3.13"`.
|
||||||
///
|
///
|
||||||
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
|
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
|
||||||
pub(crate) fn from_release_specifier(
|
pub fn from_release_specifier(
|
||||||
specifier: &VersionSpecifier,
|
specifier: &VersionSpecifier,
|
||||||
) -> Result<Self, PubGrubSpecifierError> {
|
) -> Result<Self, PubGrubSpecifierError> {
|
||||||
let ranges = match specifier.operator() {
|
let ranges = match specifier.operator() {
|
||||||
|
|
@ -106,7 +106,8 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
|
||||||
origin: Some(origin.clone()),
|
origin: Some(origin.clone()),
|
||||||
marker: requirement
|
marker: requirement
|
||||||
.marker
|
.marker
|
||||||
.and_then(|marker| marker.simplify_extras(extras)),
|
.map(|marker| marker.simplify_extras(extras))
|
||||||
|
.filter(|marker| !marker.is_true()),
|
||||||
..requirement
|
..requirement
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -129,7 +130,8 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
|
||||||
requirement.marker = requirement
|
requirement.marker = requirement
|
||||||
.marker
|
.marker
|
||||||
.take()
|
.take()
|
||||||
.and_then(|marker| marker.simplify_extras(&recursive.extras));
|
.map(|marker| marker.simplify_extras(&recursive.extras))
|
||||||
|
.filter(|marker| !marker.is_true());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ uv-distribution = { workspace = true }
|
||||||
uv-fs = { workspace = true, features = ["serde"] }
|
uv-fs = { workspace = true, features = ["serde"] }
|
||||||
uv-git = { workspace = true }
|
uv-git = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
|
uv-pubgrub = { workspace = true }
|
||||||
uv-python = { workspace = true }
|
uv-python = { workspace = true }
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
|
|
@ -55,6 +56,7 @@ textwrap = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-stream = { workspace = true }
|
tokio-stream = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
toml_edit = { workspace = true }
|
toml_edit = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use distribution_types::{BuiltDist, IndexLocations, InstalledDist, SourceDist};
|
use distribution_types::{BuiltDist, IndexLocations, InstalledDist, SourceDist};
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use pep508_rs::{MarkerTree, Requirement};
|
use pep508_rs::MarkerTree;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::candidate_selector::CandidateSelector;
|
use crate::candidate_selector::CandidateSelector;
|
||||||
|
|
@ -19,9 +19,6 @@ use crate::resolver::{IncompletePackage, ResolverMarkers, UnavailablePackage, Un
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ResolveError {
|
pub enum ResolveError {
|
||||||
#[error("Failed to find a version of `{0}` that satisfies the requirement")]
|
|
||||||
NotFound(Requirement),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Client(#[from] uv_client::Error),
|
Client(#[from] uv_client::Error),
|
||||||
|
|
||||||
|
|
@ -49,7 +46,7 @@ pub enum ResolveError {
|
||||||
#[error("Requirements contain conflicting URLs for package `{0}`:\n- {}", _1.join("\n- "))]
|
#[error("Requirements contain conflicting URLs for package `{0}`:\n- {}", _1.join("\n- "))]
|
||||||
ConflictingUrlsUniversal(PackageName, Vec<String>),
|
ConflictingUrlsUniversal(PackageName, Vec<String>),
|
||||||
|
|
||||||
#[error("Requirements contain conflicting URLs for package `{package_name}` in split `{fork_markers}`:\n- {}", urls.join("\n- "))]
|
#[error("Requirements contain conflicting URLs for package `{package_name}` in split `{fork_markers:?}`:\n- {}", urls.join("\n- "))]
|
||||||
ConflictingUrlsFork {
|
ConflictingUrlsFork {
|
||||||
package_name: PackageName,
|
package_name: PackageName,
|
||||||
urls: Vec<String>,
|
urls: Vec<String>,
|
||||||
|
|
@ -137,7 +134,7 @@ impl NoSolutionError {
|
||||||
"No solution found when resolving dependencies:".to_string()
|
"No solution found when resolving dependencies:".to_string()
|
||||||
}
|
}
|
||||||
ResolverMarkers::Fork(markers) => {
|
ResolverMarkers::Fork(markers) => {
|
||||||
format!("No solution found when resolving dependencies for split ({markers}):")
|
format!("No solution found when resolving dependencies for split ({markers:?}):")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use std::sync::Arc;
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use petgraph::visit::EdgeRef;
|
use petgraph::visit::EdgeRef;
|
||||||
|
use pubgrub::Range;
|
||||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||||
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
|
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
@ -77,6 +78,53 @@ pub struct Lock {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Lock {
|
impl Lock {
|
||||||
|
/// Deserialize the [`Lock`] from a TOML string.
|
||||||
|
pub fn from_toml(s: &str) -> Result<Lock, toml::de::Error> {
|
||||||
|
let mut lock: Lock = toml::from_str(s)?;
|
||||||
|
|
||||||
|
// Simplify all marker expressions based on the requires-python bound.
|
||||||
|
//
|
||||||
|
// This is necessary to ensure the a `Lock` deserialized from a lockfile compares
|
||||||
|
// equally to a newly created `Lock`.
|
||||||
|
// TODO(ibraheem): we should only simplify python versions when serializing or ensure
|
||||||
|
// the requires-python bound is enforced on construction to avoid this step.
|
||||||
|
if let Some(requires_python) = &lock.requires_python {
|
||||||
|
let python_version = Range::from(requires_python.bound_major_minor().clone());
|
||||||
|
let python_full_version = Range::from(requires_python.bound().clone());
|
||||||
|
|
||||||
|
for package in &mut lock.packages {
|
||||||
|
for dep in &mut package.dependencies {
|
||||||
|
if let Some(marker) = &mut dep.marker {
|
||||||
|
*marker = marker.clone().simplify_python_versions(
|
||||||
|
python_version.clone(),
|
||||||
|
python_full_version.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dep in package.optional_dependencies.values_mut().flatten() {
|
||||||
|
if let Some(marker) = &mut dep.marker {
|
||||||
|
*marker = marker.clone().simplify_python_versions(
|
||||||
|
python_version.clone(),
|
||||||
|
python_full_version.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dep in package.dev_dependencies.values_mut().flatten() {
|
||||||
|
if let Some(marker) = &mut dep.marker {
|
||||||
|
*marker = marker.clone().simplify_python_versions(
|
||||||
|
python_version.clone(),
|
||||||
|
python_full_version.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(lock)
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize a [`Lock`] from a [`ResolutionGraph`].
|
/// Initialize a [`Lock`] from a [`ResolutionGraph`].
|
||||||
pub fn from_resolution_graph(graph: &ResolutionGraph) -> Result<Self, LockError> {
|
pub fn from_resolution_graph(graph: &ResolutionGraph) -> Result<Self, LockError> {
|
||||||
let mut locked_dists = BTreeMap::new();
|
let mut locked_dists = BTreeMap::new();
|
||||||
|
|
@ -477,8 +525,12 @@ impl Lock {
|
||||||
doc.insert("requires-python", value(requires_python.to_string()));
|
doc.insert("requires-python", value(requires_python.to_string()));
|
||||||
}
|
}
|
||||||
if let Some(ref fork_markers) = self.fork_markers {
|
if let Some(ref fork_markers) = self.fork_markers {
|
||||||
let fork_markers =
|
let fork_markers = each_element_on_its_line_array(
|
||||||
each_element_on_its_line_array(fork_markers.iter().map(ToString::to_string));
|
fork_markers
|
||||||
|
.iter()
|
||||||
|
.filter_map(MarkerTree::contents)
|
||||||
|
.map(|marker| marker.to_string()),
|
||||||
|
);
|
||||||
doc.insert("environment-markers", value(fork_markers));
|
doc.insert("environment-markers", value(fork_markers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1018,10 +1070,11 @@ impl Package {
|
||||||
for dep in deps {
|
for dep in deps {
|
||||||
if let Some(mut dep) = dep.to_requirement(workspace_root, &mut dependency_extras)? {
|
if let Some(mut dep) = dep.to_requirement(workspace_root, &mut dependency_extras)? {
|
||||||
// Add back the extra marker expression.
|
// Add back the extra marker expression.
|
||||||
let marker = MarkerTree::Expression(MarkerExpression::Extra {
|
let marker = MarkerTree::expression(MarkerExpression::Extra {
|
||||||
operator: ExtraOperator::Equal,
|
operator: ExtraOperator::Equal,
|
||||||
name: extra.clone(),
|
name: extra.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
match dep.marker {
|
match dep.marker {
|
||||||
Some(ref mut tree) => tree.and(marker),
|
Some(ref mut tree) => tree.and(marker),
|
||||||
None => dep.marker = Some(marker),
|
None => dep.marker = Some(marker),
|
||||||
|
|
@ -1079,8 +1132,12 @@ impl Package {
|
||||||
self.id.to_toml(None, &mut table);
|
self.id.to_toml(None, &mut table);
|
||||||
|
|
||||||
if let Some(ref fork_markers) = self.fork_markers {
|
if let Some(ref fork_markers) = self.fork_markers {
|
||||||
let wheels =
|
let wheels = each_element_on_its_line_array(
|
||||||
each_element_on_its_line_array(fork_markers.iter().map(ToString::to_string));
|
fork_markers
|
||||||
|
.iter()
|
||||||
|
.filter_map(MarkerTree::contents)
|
||||||
|
.map(|marker| marker.to_string()),
|
||||||
|
);
|
||||||
table.insert("environment-markers", value(wheels));
|
table.insert("environment-markers", value(wheels));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2288,7 +2345,7 @@ impl Dependency {
|
||||||
.collect::<Array>();
|
.collect::<Array>();
|
||||||
table.insert("extra", value(extra_array));
|
table.insert("extra", value(extra_array));
|
||||||
}
|
}
|
||||||
if let Some(ref marker) = self.marker {
|
if let Some(marker) = self.marker.as_ref().and_then(MarkerTree::contents) {
|
||||||
table.insert("marker", value(marker.to_string()));
|
table.insert("marker", value(marker.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,11 +3,10 @@ pub(crate) use crate::pubgrub::distribution::PubGrubDistribution;
|
||||||
pub(crate) use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
|
pub(crate) use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
|
||||||
pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority};
|
pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority};
|
||||||
pub(crate) use crate::pubgrub::report::PubGrubReportFormatter;
|
pub(crate) use crate::pubgrub::report::PubGrubReportFormatter;
|
||||||
pub use crate::pubgrub::specifier::{PubGrubSpecifier, PubGrubSpecifierError};
|
pub use uv_pubgrub::{PubGrubSpecifier, PubGrubSpecifierError};
|
||||||
|
|
||||||
mod dependencies;
|
mod dependencies;
|
||||||
mod distribution;
|
mod distribution;
|
||||||
mod package;
|
mod package;
|
||||||
mod priority;
|
mod priority;
|
||||||
mod report;
|
mod report;
|
||||||
mod specifier;
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use pep508_rs::MarkerTree;
|
use pep508_rs::{MarkerTree, MarkerTreeContents};
|
||||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||||
|
|
||||||
/// [`Arc`] wrapper around [`PubGrubPackageInner`] to make cloning (inside PubGrub) cheap.
|
/// [`Arc`] wrapper around [`PubGrubPackageInner`] to make cloning (inside PubGrub) cheap.
|
||||||
|
|
@ -49,7 +49,7 @@ pub enum PubGrubPackageInner {
|
||||||
name: PackageName,
|
name: PackageName,
|
||||||
extra: Option<ExtraName>,
|
extra: Option<ExtraName>,
|
||||||
dev: Option<GroupName>,
|
dev: Option<GroupName>,
|
||||||
marker: Option<MarkerTree>,
|
marker: Option<MarkerTreeContents>,
|
||||||
},
|
},
|
||||||
/// A proxy package to represent a dependency with an extra (e.g., `black[colorama]`).
|
/// A proxy package to represent a dependency with an extra (e.g., `black[colorama]`).
|
||||||
///
|
///
|
||||||
|
|
@ -67,7 +67,7 @@ pub enum PubGrubPackageInner {
|
||||||
Extra {
|
Extra {
|
||||||
name: PackageName,
|
name: PackageName,
|
||||||
extra: ExtraName,
|
extra: ExtraName,
|
||||||
marker: Option<MarkerTree>,
|
marker: Option<MarkerTreeContents>,
|
||||||
},
|
},
|
||||||
/// A proxy package to represent an enabled "dependency group" (e.g., development dependencies).
|
/// A proxy package to represent an enabled "dependency group" (e.g., development dependencies).
|
||||||
///
|
///
|
||||||
|
|
@ -77,7 +77,7 @@ pub enum PubGrubPackageInner {
|
||||||
Dev {
|
Dev {
|
||||||
name: PackageName,
|
name: PackageName,
|
||||||
dev: GroupName,
|
dev: GroupName,
|
||||||
marker: Option<MarkerTree>,
|
marker: Option<MarkerTreeContents>,
|
||||||
},
|
},
|
||||||
/// A proxy package for a base package with a marker (e.g., `black; python_version >= "3.6"`).
|
/// A proxy package for a base package with a marker (e.g., `black; python_version >= "3.6"`).
|
||||||
///
|
///
|
||||||
|
|
@ -85,7 +85,7 @@ pub enum PubGrubPackageInner {
|
||||||
/// rather than the `Marker` variant.
|
/// rather than the `Marker` variant.
|
||||||
Marker {
|
Marker {
|
||||||
name: PackageName,
|
name: PackageName,
|
||||||
marker: MarkerTree,
|
marker: MarkerTreeContents,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,14 +94,16 @@ impl PubGrubPackage {
|
||||||
pub(crate) fn from_package(
|
pub(crate) fn from_package(
|
||||||
name: PackageName,
|
name: PackageName,
|
||||||
extra: Option<ExtraName>,
|
extra: Option<ExtraName>,
|
||||||
mut marker: Option<MarkerTree>,
|
marker: Option<MarkerTree>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Remove all extra expressions from the marker, since we track extras
|
// Remove all extra expressions from the marker, since we track extras
|
||||||
// separately. This also avoids an issue where packages added via
|
// separately. This also avoids an issue where packages added via
|
||||||
// extras end up having two distinct marker expressions, which in turn
|
// extras end up having two distinct marker expressions, which in turn
|
||||||
// makes them two distinct packages. This results in PubGrub being
|
// makes them two distinct packages. This results in PubGrub being
|
||||||
// unable to unify version constraints across such packages.
|
// unable to unify version constraints across such packages.
|
||||||
marker = marker.and_then(|m| m.simplify_extras_with(|_| true));
|
let marker = marker
|
||||||
|
.map(|m| m.simplify_extras_with(|_| true))
|
||||||
|
.and_then(|marker| marker.contents());
|
||||||
if let Some(extra) = extra {
|
if let Some(extra) = extra {
|
||||||
Self(Arc::new(PubGrubPackageInner::Extra {
|
Self(Arc::new(PubGrubPackageInner::Extra {
|
||||||
name,
|
name,
|
||||||
|
|
@ -155,8 +157,10 @@ impl PubGrubPackage {
|
||||||
PubGrubPackageInner::Root(_) | PubGrubPackageInner::Python(_) => None,
|
PubGrubPackageInner::Root(_) | PubGrubPackageInner::Python(_) => None,
|
||||||
PubGrubPackageInner::Package { marker, .. }
|
PubGrubPackageInner::Package { marker, .. }
|
||||||
| PubGrubPackageInner::Extra { marker, .. }
|
| PubGrubPackageInner::Extra { marker, .. }
|
||||||
| PubGrubPackageInner::Dev { marker, .. } => marker.as_ref(),
|
| PubGrubPackageInner::Dev { marker, .. } => {
|
||||||
PubGrubPackageInner::Marker { marker, .. } => Some(marker),
|
marker.as_ref().map(MarkerTreeContents::as_ref)
|
||||||
|
}
|
||||||
|
PubGrubPackageInner::Marker { marker, .. } => Some(marker.as_ref()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ impl RequiresPython {
|
||||||
// tree we would generate would always evaluate to
|
// tree we would generate would always evaluate to
|
||||||
// `true` because every possible Python version would
|
// `true` because every possible Python version would
|
||||||
// satisfy it.
|
// satisfy it.
|
||||||
Bound::Unbounded => return MarkerTree::And(vec![]),
|
Bound::Unbounded => return MarkerTree::TRUE,
|
||||||
Bound::Excluded(version) => (Operator::GreaterThan, version.clone().without_local()),
|
Bound::Excluded(version) => (Operator::GreaterThan, version.clone().without_local()),
|
||||||
Bound::Included(version) => {
|
Bound::Included(version) => {
|
||||||
(Operator::GreaterThanEqual, version.clone().without_local())
|
(Operator::GreaterThanEqual, version.clone().without_local())
|
||||||
|
|
@ -248,10 +248,11 @@ impl RequiresPython {
|
||||||
// impossible here).
|
// impossible here).
|
||||||
specifier: VersionSpecifier::from_version(op, version).unwrap(),
|
specifier: VersionSpecifier::from_version(op, version).unwrap(),
|
||||||
};
|
};
|
||||||
MarkerTree::And(vec![
|
|
||||||
MarkerTree::Expression(expr_python_version),
|
let mut conjunction = MarkerTree::TRUE;
|
||||||
MarkerTree::Expression(expr_python_full_version),
|
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
|
/// Returns `false` if the wheel's tags state it can't be used in the given Python version
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use pep508_rs::MarkerTree;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::resolution::{RequirementsTxtDist, ResolutionGraphNode};
|
use crate::resolution::{RequirementsTxtDist, ResolutionGraphNode};
|
||||||
use crate::{marker, ResolutionGraph, ResolverMarkers};
|
use crate::{ResolutionGraph, ResolverMarkers};
|
||||||
|
|
||||||
static UNIVERSAL_MARKERS: ResolverMarkers = ResolverMarkers::Universal {
|
static UNIVERSAL_MARKERS: ResolverMarkers = ResolverMarkers::Universal {
|
||||||
fork_preferences: None,
|
fork_preferences: None,
|
||||||
|
|
@ -410,7 +410,7 @@ fn propagate_markers(mut graph: IntermediatePetGraph) -> IntermediatePetGraph {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let DisplayResolutionGraphNode::Dist(node) = &mut graph[index] {
|
if let DisplayResolutionGraphNode::Dist(node) = &mut graph[index] {
|
||||||
node.markers = marker_tree.and_then(|marker| marker::normalize(marker, None));
|
node.markers = marker_tree;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use petgraph::{
|
||||||
graph::{Graph, NodeIndex},
|
graph::{Graph, NodeIndex},
|
||||||
Directed,
|
Directed,
|
||||||
};
|
};
|
||||||
|
use pubgrub::Range;
|
||||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
|
|
@ -12,7 +13,7 @@ use distribution_types::{
|
||||||
VersionOrUrlRef,
|
VersionOrUrlRef,
|
||||||
};
|
};
|
||||||
use pep440_rs::{Version, VersionSpecifier};
|
use pep440_rs::{Version, VersionSpecifier};
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl};
|
use pep508_rs::{MarkerEnvironment, MarkerTree, MarkerTreeKind, VerbatimUrl};
|
||||||
use pypi_types::{HashDigest, ParsedUrlError, Requirement, VerbatimParsedUrl, Yanked};
|
use pypi_types::{HashDigest, ParsedUrlError, Requirement, VerbatimParsedUrl, Yanked};
|
||||||
use uv_configuration::{Constraints, Overrides};
|
use uv_configuration::{Constraints, Overrides};
|
||||||
use uv_distribution::Metadata;
|
use uv_distribution::Metadata;
|
||||||
|
|
@ -158,12 +159,14 @@ impl ResolutionGraph {
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
// Normalize any markers.
|
// Normalize any markers.
|
||||||
for edge in petgraph.edge_indices() {
|
if let Some(ref requires_python) = requires_python {
|
||||||
if let Some(marker) = petgraph[edge].take() {
|
for edge in petgraph.edge_indices() {
|
||||||
petgraph[edge] = crate::marker::normalize(
|
if let Some(marker) = petgraph[edge].take() {
|
||||||
marker,
|
petgraph[edge] = Some(marker.simplify_python_versions(
|
||||||
requires_python.as_ref().map(RequiresPython::bound),
|
Range::from(requires_python.bound_major_minor().clone()),
|
||||||
);
|
Range::from(requires_python.bound().clone()),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,6 +188,8 @@ impl ResolutionGraph {
|
||||||
.expect("A non-forking resolution exists in forking mode")
|
.expect("A non-forking resolution exists in forking mode")
|
||||||
.clone()
|
.clone()
|
||||||
})
|
})
|
||||||
|
// Any unsatisfiable forks were skipped.
|
||||||
|
.filter(|fork| !fork.is_false())
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -511,16 +516,31 @@ impl ResolutionGraph {
|
||||||
|
|
||||||
/// Add all marker parameters from the given tree to the given set.
|
/// Add all marker parameters from the given tree to the given set.
|
||||||
fn add_marker_params_from_tree(marker_tree: &MarkerTree, set: &mut IndexSet<MarkerParam>) {
|
fn add_marker_params_from_tree(marker_tree: &MarkerTree, set: &mut IndexSet<MarkerParam>) {
|
||||||
match marker_tree {
|
match marker_tree.kind() {
|
||||||
MarkerTree::Expression(MarkerExpression::Version { key, .. }) => {
|
MarkerTreeKind::True => {}
|
||||||
set.insert(MarkerParam::Version(key.clone()));
|
MarkerTreeKind::False => {}
|
||||||
|
MarkerTreeKind::Version(marker) => {
|
||||||
|
set.insert(MarkerParam::Version(marker.key().clone()));
|
||||||
|
for (_, tree) in marker.edges() {
|
||||||
|
add_marker_params_from_tree(&tree, set);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MarkerTree::Expression(MarkerExpression::String { key, .. }) => {
|
MarkerTreeKind::String(marker) => {
|
||||||
set.insert(MarkerParam::String(key.clone()));
|
set.insert(MarkerParam::String(marker.key().clone()));
|
||||||
|
for (_, tree) in marker.children() {
|
||||||
|
add_marker_params_from_tree(&tree, set);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MarkerTree::And(ref exprs) | MarkerTree::Or(ref exprs) => {
|
MarkerTreeKind::In(marker) => {
|
||||||
for expr in exprs {
|
set.insert(MarkerParam::String(marker.key().clone()));
|
||||||
add_marker_params_from_tree(expr, set);
|
for (_, tree) in marker.children() {
|
||||||
|
add_marker_params_from_tree(&tree, set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MarkerTreeKind::Contains(marker) => {
|
||||||
|
set.insert(MarkerParam::String(marker.key().clone()));
|
||||||
|
for (_, tree) in marker.children() {
|
||||||
|
add_marker_params_from_tree(&tree, set);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We specifically don't care about these for the
|
// We specifically don't care about these for the
|
||||||
|
|
@ -528,9 +548,11 @@ impl ResolutionGraph {
|
||||||
// file. Quoted strings are marker values given by the
|
// file. Quoted strings are marker values given by the
|
||||||
// user. We don't track those here, since we're only
|
// user. We don't track those here, since we're only
|
||||||
// interested in which markers are used.
|
// interested in which markers are used.
|
||||||
MarkerTree::Expression(
|
MarkerTreeKind::Extra(marker) => {
|
||||||
MarkerExpression::Extra { .. } | MarkerExpression::Arbitrary { .. },
|
for (_, tree) in marker.children() {
|
||||||
) => {}
|
add_marker_params_from_tree(&tree, set);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -579,7 +601,7 @@ impl ResolutionGraph {
|
||||||
|
|
||||||
// Generate the final marker expression as a conjunction of
|
// Generate the final marker expression as a conjunction of
|
||||||
// strict equality terms.
|
// strict equality terms.
|
||||||
let mut conjuncts = vec![];
|
let mut conjunction = MarkerTree::TRUE;
|
||||||
for marker_param in seen_marker_values {
|
for marker_param in seen_marker_values {
|
||||||
let expr = match marker_param {
|
let expr = match marker_param {
|
||||||
MarkerParam::Version(value_version) => {
|
MarkerParam::Version(value_version) => {
|
||||||
|
|
@ -598,9 +620,9 @@ impl ResolutionGraph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
conjuncts.push(MarkerTree::Expression(expr));
|
conjunction.and(MarkerTree::expression(expr));
|
||||||
}
|
}
|
||||||
Ok(MarkerTree::And(conjuncts))
|
Ok(conjunction)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If there are multiple distributions for the same package name, return the markers of the
|
/// If there are multiple distributions for the same package name, return the markers of the
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,11 @@ impl RequirementsTxtDist {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(given) = given {
|
if let Some(given) = given {
|
||||||
return if let Some(markers) = self.markers.as_ref().filter(|_| include_markers)
|
return if let Some(markers) = self
|
||||||
|
.markers
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| include_markers)
|
||||||
|
.and_then(MarkerTree::contents)
|
||||||
{
|
{
|
||||||
Cow::Owned(format!("{given} ; {markers}"))
|
Cow::Owned(format!("{given} ; {markers}"))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -100,7 +104,12 @@ impl RequirementsTxtDist {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.extras.is_empty() || !include_extras {
|
if self.extras.is_empty() || !include_extras {
|
||||||
if let Some(markers) = self.markers.as_ref().filter(|_| include_markers) {
|
if let Some(markers) = self
|
||||||
|
.markers
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| include_markers)
|
||||||
|
.and_then(MarkerTree::contents)
|
||||||
|
{
|
||||||
Cow::Owned(format!("{} ; {}", self.dist.verbatim(), markers))
|
Cow::Owned(format!("{} ; {}", self.dist.verbatim(), markers))
|
||||||
} else {
|
} else {
|
||||||
self.dist.verbatim()
|
self.dist.verbatim()
|
||||||
|
|
@ -109,7 +118,12 @@ impl RequirementsTxtDist {
|
||||||
let mut extras = self.extras.clone();
|
let mut extras = self.extras.clone();
|
||||||
extras.sort_unstable();
|
extras.sort_unstable();
|
||||||
extras.dedup();
|
extras.dedup();
|
||||||
if let Some(markers) = self.markers.as_ref().filter(|_| include_markers) {
|
if let Some(markers) = self
|
||||||
|
.markers
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| include_markers)
|
||||||
|
.and_then(MarkerTree::contents)
|
||||||
|
{
|
||||||
Cow::Owned(format!(
|
Cow::Owned(format!(
|
||||||
"{}[{}]{} ; {}",
|
"{}[{}]{} ; {}",
|
||||||
self.name(),
|
self.name(),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ use pep508_rs::{MarkerTree, PackageName};
|
||||||
use pypi_types::Requirement;
|
use pypi_types::Requirement;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::marker::is_disjoint;
|
|
||||||
use crate::ResolverMarkers;
|
use crate::ResolverMarkers;
|
||||||
|
|
||||||
/// A set of package names associated with a given fork.
|
/// A set of package names associated with a given fork.
|
||||||
|
|
@ -72,7 +71,7 @@ impl<T> ForkMap<T> {
|
||||||
!entry
|
!entry
|
||||||
.marker
|
.marker
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|marker| is_disjoint(fork, marker))
|
.is_some_and(|marker| fork.is_disjoint(marker))
|
||||||
})
|
})
|
||||||
.map(|entry| &entry.value)
|
.map(|entry| &entry.value)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ use crate::dependency_provider::UvDependencyProvider;
|
||||||
use crate::error::{NoSolutionError, ResolveError};
|
use crate::error::{NoSolutionError, ResolveError};
|
||||||
use crate::fork_urls::ForkUrls;
|
use crate::fork_urls::ForkUrls;
|
||||||
use crate::manifest::Manifest;
|
use crate::manifest::Manifest;
|
||||||
use crate::marker::{normalize, requires_python_marker};
|
|
||||||
use crate::pins::FilePins;
|
use crate::pins::FilePins;
|
||||||
use crate::preferences::Preferences;
|
use crate::preferences::Preferences;
|
||||||
use crate::pubgrub::{
|
use crate::pubgrub::{
|
||||||
|
|
@ -69,7 +68,7 @@ pub use crate::resolver::provider::{
|
||||||
use crate::resolver::reporter::Facade;
|
use crate::resolver::reporter::Facade;
|
||||||
pub use crate::resolver::reporter::{BuildId, Reporter};
|
pub use crate::resolver::reporter::{BuildId, Reporter};
|
||||||
use crate::yanks::AllowedYanks;
|
use crate::yanks::AllowedYanks;
|
||||||
use crate::{DependencyMode, Exclusions, FlatIndex, Options};
|
use crate::{marker, DependencyMode, Exclusions, FlatIndex, Options};
|
||||||
|
|
||||||
mod availability;
|
mod availability;
|
||||||
mod batch_prefetch;
|
mod batch_prefetch;
|
||||||
|
|
@ -342,11 +341,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
if let ResolverMarkers::Fork(markers) = &state.markers {
|
if let ResolverMarkers::Fork(markers) = &state.markers {
|
||||||
if let Some(requires_python) = state.requires_python.as_ref() {
|
if let Some(requires_python) = state.requires_python.as_ref() {
|
||||||
debug!(
|
debug!(
|
||||||
"Solving split {} (requires-python: {})",
|
"Solving split {:?} (requires-python: {:?})",
|
||||||
markers, requires_python
|
markers, requires_python
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debug!("Solving split {}", markers);
|
debug!("Solving split {:?}", markers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
@ -413,9 +412,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
.fork_markers()
|
.fork_markers()
|
||||||
.expect("A non-forking resolution exists in forking mode")
|
.expect("A non-forking resolution exists in forking mode")
|
||||||
.clone());
|
.clone());
|
||||||
existing_resolution.markers = normalize(new_markers, None)
|
existing_resolution.markers = if new_markers.is_true() {
|
||||||
.map(ResolverMarkers::Fork)
|
ResolverMarkers::universal(None)
|
||||||
.unwrap_or(ResolverMarkers::universal(None));
|
} else {
|
||||||
|
ResolverMarkers::Fork(new_markers)
|
||||||
|
};
|
||||||
continue 'FORK;
|
continue 'FORK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -612,7 +613,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
for resolution in &resolutions {
|
for resolution in &resolutions {
|
||||||
if let Some(markers) = resolution.markers.fork_markers() {
|
if let Some(markers) = resolution.markers.fork_markers() {
|
||||||
debug!(
|
debug!(
|
||||||
"Distinct solution for ({markers}) with {} packages",
|
"Distinct solution for ({markers:?}) with {} packages",
|
||||||
resolution.nodes.len()
|
resolution.nodes.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -668,7 +669,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
if let Some(ref dev) = edge.to_dev {
|
if let Some(ref dev) = edge.to_dev {
|
||||||
write!(msg, " (group: {dev})").unwrap();
|
write!(msg, " (group: {dev})").unwrap();
|
||||||
}
|
}
|
||||||
if let Some(ref marker) = edge.marker {
|
if let Some(marker) = edge.marker.as_ref().and_then(MarkerTree::contents) {
|
||||||
write!(msg, " ; {marker}").unwrap();
|
write!(msg, " ; {marker}").unwrap();
|
||||||
}
|
}
|
||||||
trace!("Resolution: {msg}");
|
trace!("Resolution: {msg}");
|
||||||
|
|
@ -1509,7 +1510,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
// supported by the root, skip it.
|
// supported by the root, skip it.
|
||||||
if !satisfies_requires_python(requires_python, requirement) {
|
if !satisfies_requires_python(requires_python, requirement) {
|
||||||
trace!(
|
trace!(
|
||||||
"skipping {requirement} because of Requires-Python {requires_python}",
|
"skipping {requirement} because of Requires-Python {requires_python:?}",
|
||||||
// OK because this filter only applies when there is a present
|
// OK because this filter only applies when there is a present
|
||||||
// Requires-Python specifier.
|
// Requires-Python specifier.
|
||||||
requires_python = requires_python.unwrap()
|
requires_python = requires_python.unwrap()
|
||||||
|
|
@ -1521,7 +1522,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
// this fork (but will be part of another fork).
|
// this fork (but will be part of another fork).
|
||||||
if let ResolverMarkers::Fork(markers) = markers {
|
if let ResolverMarkers::Fork(markers) = markers {
|
||||||
if !possible_to_satisfy_markers(markers, requirement) {
|
if !possible_to_satisfy_markers(markers, requirement) {
|
||||||
trace!("skipping {requirement} because of context resolver markers {markers}");
|
trace!("skipping {requirement} because of context resolver markers {markers:?}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1558,7 +1559,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
.filter(move |constraint| {
|
.filter(move |constraint| {
|
||||||
if !satisfies_requires_python(requires_python, constraint) {
|
if !satisfies_requires_python(requires_python, constraint) {
|
||||||
trace!(
|
trace!(
|
||||||
"skipping {constraint} because of Requires-Python {requires_python}",
|
"skipping {constraint} because of Requires-Python {requires_python:?}",
|
||||||
requires_python = requires_python.unwrap()
|
requires_python = requires_python.unwrap()
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1568,7 +1569,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
// this fork (but will be part of another fork).
|
// this fork (but will be part of another fork).
|
||||||
if let ResolverMarkers::Fork(markers) = markers {
|
if let ResolverMarkers::Fork(markers) = markers {
|
||||||
if !possible_to_satisfy_markers(markers, constraint) {
|
if !possible_to_satisfy_markers(markers, constraint) {
|
||||||
trace!("skipping {constraint} because of context resolver markers {markers}");
|
trace!("skipping {constraint} because of context resolver markers {markers:?}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1598,7 +1599,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
// should only apply when _both_ markers are true.
|
// should only apply when _both_ markers are true.
|
||||||
if let Some(marker) = requirement.marker.as_ref() {
|
if let Some(marker) = requirement.marker.as_ref() {
|
||||||
let marker = constraint.marker.as_ref().map(|m| {
|
let marker = constraint.marker.as_ref().map(|m| {
|
||||||
MarkerTree::And(vec![marker.clone(), m.clone()])
|
let mut marker = marker.clone();
|
||||||
|
marker.and(m.clone());
|
||||||
|
marker
|
||||||
}).or_else(|| Some(marker.clone()));
|
}).or_else(|| Some(marker.clone()));
|
||||||
|
|
||||||
Cow::Owned(Requirement {
|
Cow::Owned(Requirement {
|
||||||
|
|
@ -2224,11 +2227,9 @@ impl ForkState {
|
||||||
/// accordingly.
|
/// accordingly.
|
||||||
fn with_markers(mut self, markers: MarkerTree) -> Self {
|
fn with_markers(mut self, markers: MarkerTree) -> Self {
|
||||||
let combined_markers = self.markers.and(markers);
|
let combined_markers = self.markers.and(markers);
|
||||||
let combined_markers =
|
|
||||||
normalize(combined_markers, None).unwrap_or_else(|| MarkerTree::And(vec![]));
|
|
||||||
|
|
||||||
// If the fork contains a narrowed Python requirement, apply it.
|
// If the fork contains a narrowed Python requirement, apply it.
|
||||||
let python_requirement = requires_python_marker(&combined_markers)
|
let python_requirement = marker::requires_python(&combined_markers)
|
||||||
.and_then(|marker| self.python_requirement.narrow(&marker));
|
.and_then(|marker| self.python_requirement.narrow(&marker));
|
||||||
if let Some(python_requirement) = python_requirement {
|
if let Some(python_requirement) = python_requirement {
|
||||||
if let Some(target) = python_requirement.target() {
|
if let Some(target) = python_requirement.target() {
|
||||||
|
|
@ -2363,7 +2364,7 @@ impl ForkState {
|
||||||
to_url: to_url.cloned(),
|
to_url: to_url.cloned(),
|
||||||
to_extra: None,
|
to_extra: None,
|
||||||
to_dev: None,
|
to_dev: None,
|
||||||
marker: Some(dependency_marker.clone()),
|
marker: Some(dependency_marker.as_ref().clone()),
|
||||||
};
|
};
|
||||||
edges.insert(edge);
|
edges.insert(edge);
|
||||||
}
|
}
|
||||||
|
|
@ -2389,7 +2390,7 @@ impl ForkState {
|
||||||
to_url: to_url.cloned(),
|
to_url: to_url.cloned(),
|
||||||
to_extra: Some(dependency_extra.clone()),
|
to_extra: Some(dependency_extra.clone()),
|
||||||
to_dev: None,
|
to_dev: None,
|
||||||
marker: dependency_marker.clone(),
|
marker: dependency_marker.clone().map(MarkerTree::from),
|
||||||
};
|
};
|
||||||
edges.insert(edge);
|
edges.insert(edge);
|
||||||
}
|
}
|
||||||
|
|
@ -2415,7 +2416,7 @@ impl ForkState {
|
||||||
to_url: to_url.cloned(),
|
to_url: to_url.cloned(),
|
||||||
to_extra: None,
|
to_extra: None,
|
||||||
to_dev: Some(dependency_dev.clone()),
|
to_dev: Some(dependency_dev.clone()),
|
||||||
marker: dependency_marker.clone(),
|
marker: dependency_marker.clone().map(MarkerTree::from),
|
||||||
};
|
};
|
||||||
edges.insert(edge);
|
edges.insert(edge);
|
||||||
}
|
}
|
||||||
|
|
@ -2726,7 +2727,7 @@ impl Dependencies {
|
||||||
}
|
}
|
||||||
let mut forks = vec![Fork {
|
let mut forks = vec![Fork {
|
||||||
dependencies: vec![],
|
dependencies: vec![],
|
||||||
markers: MarkerTree::And(vec![]),
|
markers: MarkerTree::TRUE,
|
||||||
}];
|
}];
|
||||||
let mut diverging_packages = Vec::new();
|
let mut diverging_packages = Vec::new();
|
||||||
for (name, possible_forks) in by_name {
|
for (name, possible_forks) in by_name {
|
||||||
|
|
@ -2748,7 +2749,7 @@ impl Dependencies {
|
||||||
assert!(fork_groups.forks.len() >= 2, "expected definitive fork");
|
assert!(fork_groups.forks.len() >= 2, "expected definitive fork");
|
||||||
let mut new_forks: Vec<Fork> = vec![];
|
let mut new_forks: Vec<Fork> = vec![];
|
||||||
if let Some(markers) = fork_groups.remaining_universe() {
|
if let Some(markers) = fork_groups.remaining_universe() {
|
||||||
trace!("Adding split to cover possibly incomplete markers: {markers}");
|
trace!("Adding split to cover possibly incomplete markers: {markers:?}");
|
||||||
let mut new_forks_for_remaining_universe = forks.clone();
|
let mut new_forks_for_remaining_universe = forks.clone();
|
||||||
for fork in &mut new_forks_for_remaining_universe {
|
for fork in &mut new_forks_for_remaining_universe {
|
||||||
fork.markers.and(markers.clone());
|
fork.markers.and(markers.clone());
|
||||||
|
|
@ -2847,8 +2848,8 @@ impl Ord for Fork {
|
||||||
// A higher `requires-python` requirement indicates a _lower-priority_ fork. We'd prefer
|
// A higher `requires-python` requirement indicates a _lower-priority_ fork. We'd prefer
|
||||||
// to solve `<3.7` before solving `>=3.7`, since the resolution produced by the former might
|
// to solve `<3.7` before solving `>=3.7`, since the resolution produced by the former might
|
||||||
// work for the latter, but the inverse is unlikely to be true.
|
// work for the latter, but the inverse is unlikely to be true.
|
||||||
let self_bound = requires_python_marker(&self.markers).unwrap_or_default();
|
let self_bound = marker::requires_python(&self.markers).unwrap_or_default();
|
||||||
let other_bound = requires_python_marker(&other.markers).unwrap_or_default();
|
let other_bound = marker::requires_python(&other.markers).unwrap_or_default();
|
||||||
|
|
||||||
other_bound.cmp(&self_bound).then_with(|| {
|
other_bound.cmp(&self_bound).then_with(|| {
|
||||||
// If there's no difference, prioritize forks with upper bounds. We'd prefer to solve
|
// If there's no difference, prioritize forks with upper bounds. We'd prefer to solve
|
||||||
|
|
@ -2894,12 +2895,10 @@ impl Fork {
|
||||||
/// It is only added if the markers on the given package are not disjoint
|
/// It is only added if the markers on the given package are not disjoint
|
||||||
/// with this fork's markers.
|
/// with this fork's markers.
|
||||||
fn add_nonfork_package(&mut self, dependency: PubGrubDependency) {
|
fn add_nonfork_package(&mut self, dependency: PubGrubDependency) {
|
||||||
use crate::marker::is_disjoint;
|
|
||||||
|
|
||||||
if dependency
|
if dependency
|
||||||
.package
|
.package
|
||||||
.marker()
|
.marker()
|
||||||
.map_or(true, |marker| !is_disjoint(marker, &self.markers))
|
.map_or(true, |marker| !marker.is_disjoint(&self.markers))
|
||||||
{
|
{
|
||||||
self.dependencies.push(dependency);
|
self.dependencies.push(dependency);
|
||||||
}
|
}
|
||||||
|
|
@ -2908,13 +2907,11 @@ impl Fork {
|
||||||
/// Removes any dependencies in this fork whose markers are disjoint with
|
/// Removes any dependencies in this fork whose markers are disjoint with
|
||||||
/// its own markers.
|
/// its own markers.
|
||||||
fn remove_disjoint_packages(&mut self) {
|
fn remove_disjoint_packages(&mut self) {
|
||||||
use crate::marker::is_disjoint;
|
|
||||||
|
|
||||||
self.dependencies.retain(|dependency| {
|
self.dependencies.retain(|dependency| {
|
||||||
dependency
|
dependency
|
||||||
.package
|
.package
|
||||||
.marker()
|
.marker()
|
||||||
.map_or(true, |pkg_marker| !is_disjoint(pkg_marker, &self.markers))
|
.map_or(true, |pkg_marker| !pkg_marker.is_disjoint(&self.markers))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3041,9 +3038,13 @@ impl<'a> PossibleForkGroups<'a> {
|
||||||
/// `None` is returned. But note that if a marker tree is returned, it is
|
/// `None` is returned. But note that if a marker tree is returned, it is
|
||||||
/// still possible for it to describe exactly zero marker environments.
|
/// still possible for it to describe exactly zero marker environments.
|
||||||
fn remaining_universe(&self) -> Option<MarkerTree> {
|
fn remaining_universe(&self) -> Option<MarkerTree> {
|
||||||
let have = MarkerTree::Or(self.forks.iter().map(PossibleFork::union).collect());
|
let mut have = MarkerTree::FALSE;
|
||||||
|
for fork in &self.forks {
|
||||||
|
have.or(fork.union());
|
||||||
|
}
|
||||||
|
|
||||||
let missing = have.negate();
|
let missing = have.negate();
|
||||||
if crate::marker::is_definitively_empty_set(&missing) {
|
if missing.is_false() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(missing)
|
Some(missing)
|
||||||
|
|
@ -3074,10 +3075,8 @@ impl<'a> PossibleFork<'a> {
|
||||||
/// intersection with *any* of the package markers within this possible
|
/// intersection with *any* of the package markers within this possible
|
||||||
/// fork.
|
/// fork.
|
||||||
fn is_overlapping(&self, candidate_package_markers: &MarkerTree) -> bool {
|
fn is_overlapping(&self, candidate_package_markers: &MarkerTree) -> bool {
|
||||||
use crate::marker::is_disjoint;
|
|
||||||
|
|
||||||
for (_, package_markers) in &self.packages {
|
for (_, package_markers) in &self.packages {
|
||||||
if !is_disjoint(candidate_package_markers, package_markers) {
|
if !candidate_package_markers.is_disjoint(package_markers) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3089,16 +3088,11 @@ impl<'a> PossibleFork<'a> {
|
||||||
/// Each marker expression in the union returned is guaranteed to be overlapping
|
/// Each marker expression in the union returned is guaranteed to be overlapping
|
||||||
/// with at least one other expression in the same union.
|
/// with at least one other expression in the same union.
|
||||||
fn union(&self) -> MarkerTree {
|
fn union(&self) -> MarkerTree {
|
||||||
let mut trees: Vec<MarkerTree> = self
|
let mut union = MarkerTree::FALSE;
|
||||||
.packages
|
for &(_, marker) in &self.packages {
|
||||||
.iter()
|
union.or(marker.clone());
|
||||||
.map(|&(_, tree)| (*tree).clone())
|
|
||||||
.collect();
|
|
||||||
if trees.len() == 1 {
|
|
||||||
trees.pop().unwrap()
|
|
||||||
} else {
|
|
||||||
MarkerTree::Or(trees)
|
|
||||||
}
|
}
|
||||||
|
union
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3124,5 +3118,5 @@ fn possible_to_satisfy_markers(markers: &MarkerTree, requirement: &Requirement)
|
||||||
let Some(marker) = requirement.marker.as_ref() else {
|
let Some(marker) = requirement.marker.as_ref() else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
!crate::marker::is_disjoint(markers, marker)
|
!markers.is_disjoint(marker)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ impl Display for ResolverMarkers {
|
||||||
ResolverMarkers::Universal { .. } => f.write_str("universal"),
|
ResolverMarkers::Universal { .. } => f.write_str("universal"),
|
||||||
ResolverMarkers::SpecificEnvironment(_) => f.write_str("specific environment"),
|
ResolverMarkers::SpecificEnvironment(_) => f.write_str("specific environment"),
|
||||||
ResolverMarkers::Fork(markers) => {
|
ResolverMarkers::Fork(markers) => {
|
||||||
write!(f, "({markers})")
|
write!(f, "({markers:?})")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -407,12 +407,14 @@ pub(crate) async fn pip_compile(
|
||||||
if include_marker_expression {
|
if include_marker_expression {
|
||||||
if let ResolverMarkers::SpecificEnvironment(markers) = &markers {
|
if let ResolverMarkers::SpecificEnvironment(markers) = &markers {
|
||||||
let relevant_markers = resolution.marker_tree(&top_level_index, markers)?;
|
let relevant_markers = resolution.marker_tree(&top_level_index, markers)?;
|
||||||
writeln!(
|
if let Some(relevant_markers) = relevant_markers.contents() {
|
||||||
writer,
|
writeln!(
|
||||||
"{}",
|
writer,
|
||||||
"# Pinned dependencies known to be valid for:".green()
|
"{}",
|
||||||
)?;
|
"# Pinned dependencies known to be valid for:".green()
|
||||||
writeln!(writer, "{}", format!("# {relevant_markers}").green())?;
|
)?;
|
||||||
|
writeln!(writer, "{}", format!("# {relevant_markers}").green())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -594,7 +594,7 @@ pub(crate) async fn commit(lock: &Lock, workspace: &Workspace) -> Result<(), Pro
|
||||||
/// Returns `Ok(None)` if the lockfile does not exist.
|
/// Returns `Ok(None)` if the lockfile does not exist.
|
||||||
pub(crate) async fn read(workspace: &Workspace) -> Result<Option<Lock>, ProjectError> {
|
pub(crate) async fn read(workspace: &Workspace) -> Result<Option<Lock>, ProjectError> {
|
||||||
match fs_err::tokio::read_to_string(&workspace.install_path().join("uv.lock")).await {
|
match fs_err::tokio::read_to_string(&workspace.install_path().join("uv.lock")).await {
|
||||||
Ok(encoded) => match toml::from_str::<Lock>(&encoded) {
|
Ok(encoded) => match Lock::from_toml(&encoded) {
|
||||||
Ok(lock) => Ok(Some(lock)),
|
Ok(lock) => Ok(Some(lock)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprint!("Failed to parse lockfile; ignoring locked requirements: {err}");
|
eprint!("Failed to parse lockfile; ignoring locked requirements: {err}");
|
||||||
|
|
|
||||||
|
|
@ -2129,8 +2129,8 @@ fn lock_upgrade_log_multi_version() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'win32'",
|
|
||||||
"sys_platform != 'win32'",
|
"sys_platform != 'win32'",
|
||||||
|
"sys_platform == 'win32'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
|
@ -3586,14 +3586,12 @@ fn lock_python_version_marker_complement() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"python_full_version <= '3.10' and python_version == '3.10'",
|
"python_full_version > '3.10' and python_version > '3.10'",
|
||||||
"python_full_version <= '3.10' and python_version < '3.10'",
|
|
||||||
"python_full_version <= '3.10' and python_version < '3.10' and python_version > '3.10'",
|
|
||||||
"python_full_version <= '3.10' and python_version > '3.10'",
|
|
||||||
"python_full_version > '3.10' and python_version == '3.10'",
|
"python_full_version > '3.10' and python_version == '3.10'",
|
||||||
"python_full_version > '3.10' and python_version < '3.10'",
|
"python_full_version > '3.10' and python_version < '3.10'",
|
||||||
"python_full_version > '3.10' and python_version < '3.10' and python_version > '3.10'",
|
"python_full_version <= '3.10' and python_version > '3.10'",
|
||||||
"python_full_version > '3.10' and python_version > '3.10'",
|
"python_full_version <= '3.10' and python_version == '3.10'",
|
||||||
|
"python_full_version <= '3.10' and python_version < '3.10'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
|
@ -3624,7 +3622,7 @@ fn lock_python_version_marker_complement() -> Result<()> {
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "attrs" },
|
{ name = "attrs" },
|
||||||
{ name = "iniconfig" },
|
{ name = "iniconfig" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version <= '3.10' or python_full_version > '3.10'" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4791,8 +4789,8 @@ fn lock_same_version_multiple_urls() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform != 'darwin'",
|
"sys_platform != 'darwin'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,8 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -295,8 +295,8 @@ fn fork_basic() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -580,8 +580,8 @@ fn fork_filter_sibling_dependencies() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -873,7 +873,7 @@ fn fork_incomplete_markers() -> Result<()> {
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"python_version < '3.10'",
|
"python_version < '3.10'",
|
||||||
"python_version >= '3.11'",
|
"python_version >= '3.11'",
|
||||||
"python_version < '3.11' and python_version >= '3.10'",
|
"python_version >= '3.10' and python_version < '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -928,7 +928,7 @@ fn fork_incomplete_markers() -> Result<()> {
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_version < '3.10'" },
|
{ name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_version < '3.10'" },
|
||||||
{ name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_version >= '3.11'" },
|
{ name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "python_version >= '3.11'" },
|
||||||
{ name = "package-b", marker = "python_version < '3.10' or python_version >= '3.11' or (python_version < '3.11' and python_version >= '3.10')" },
|
{ name = "package-b" },
|
||||||
]
|
]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
@ -1221,10 +1221,10 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> {
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
|
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1232,8 +1232,8 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> {
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
|
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
||||||
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
@ -1265,7 +1265,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> {
|
||||||
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "package-c", marker = "implementation_name == 'pypy' or sys_platform == 'linux'" },
|
{ name = "package-c", marker = "sys_platform == 'linux' or implementation_name == 'pypy'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_allowed_b-1.0.0.tar.gz", hash = "sha256:d6bd196a0a152c1b32e09f08e554d22ae6a6b3b916e39ad4552572afae5f5492" }
|
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_allowed_b-1.0.0.tar.gz", hash = "sha256:d6bd196a0a152c1b32e09f08e554d22ae6a6b3b916e39ad4552572afae5f5492" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|
@ -1397,10 +1397,10 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> {
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
|
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1408,8 +1408,8 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> {
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
|
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
||||||
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
@ -1562,10 +1562,10 @@ fn fork_marker_inherit_combined() -> Result<()> {
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
|
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1573,8 +1573,8 @@ fn fork_marker_inherit_combined() -> Result<()> {
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
|
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
|
||||||
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
"implementation_name == 'pypy' and sys_platform == 'darwin'",
|
||||||
|
"implementation_name == 'cpython' and sys_platform == 'darwin'",
|
||||||
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
@ -1719,8 +1719,8 @@ fn fork_marker_inherit_isolated() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1863,8 +1863,8 @@ fn fork_marker_inherit_transitive() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2015,8 +2015,8 @@ fn fork_marker_inherit() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2149,8 +2149,8 @@ fn fork_marker_limited_inherit() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2206,7 +2206,7 @@ fn fork_marker_limited_inherit() -> Result<()> {
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" },
|
{ name = "package-a", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" },
|
||||||
{ name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" },
|
{ name = "package-a", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" },
|
||||||
{ name = "package-b", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (sys_platform != 'darwin' and sys_platform != 'linux')" },
|
{ name = "package-b" },
|
||||||
]
|
]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
@ -2299,8 +2299,8 @@ fn fork_marker_selection() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2342,7 +2342,7 @@ fn fork_marker_selection() -> Result<()> {
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "package-a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (sys_platform != 'darwin' and sys_platform != 'linux')" },
|
{ name = "package-a" },
|
||||||
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" },
|
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" },
|
||||||
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" },
|
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
|
|
@ -2449,8 +2449,8 @@ fn fork_marker_track() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'darwin'",
|
|
||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
"sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2504,7 +2504,7 @@ fn fork_marker_track() -> Result<()> {
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "package-a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (sys_platform != 'darwin' and sys_platform != 'linux')" },
|
{ name = "package-a" },
|
||||||
{ name = "package-b", version = "2.7", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" },
|
{ name = "package-b", version = "2.7", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'darwin'" },
|
||||||
{ name = "package-b", version = "2.8", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" },
|
{ name = "package-b", version = "2.8", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
|
|
@ -3056,8 +3056,8 @@ fn preferences_dependent_forking_bistable() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'linux'",
|
|
||||||
"sys_platform != 'linux'",
|
"sys_platform != 'linux'",
|
||||||
|
"sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3434,8 +3434,8 @@ fn preferences_dependent_forking_tristable() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'linux'",
|
|
||||||
"sys_platform != 'linux'",
|
"sys_platform != 'linux'",
|
||||||
|
"sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3712,8 +3712,8 @@ fn preferences_dependent_forking() -> Result<()> {
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'linux'",
|
|
||||||
"sys_platform != 'linux'",
|
"sys_platform != 'linux'",
|
||||||
|
"sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3882,10 +3882,10 @@ fn fork_remaining_universe_partitioning() -> Result<()> {
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
environment-markers = [
|
environment-markers = [
|
||||||
"sys_platform == 'windows'",
|
"sys_platform == 'windows'",
|
||||||
|
"sys_platform != 'illumos' and sys_platform != 'windows'",
|
||||||
"os_name == 'darwin' and sys_platform == 'illumos'",
|
"os_name == 'darwin' and sys_platform == 'illumos'",
|
||||||
"os_name == 'linux' and sys_platform == 'illumos'",
|
"os_name == 'linux' and sys_platform == 'illumos'",
|
||||||
"os_name != 'darwin' and os_name != 'linux' and sys_platform == 'illumos'",
|
"os_name != 'darwin' and os_name != 'linux' and sys_platform == 'illumos'",
|
||||||
"sys_platform != 'illumos' and sys_platform != 'windows'",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -6471,23 +6471,23 @@ fn no_strip_markers_multiple_markers() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --no-strip-markers --python-platform windows
|
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --no-strip-markers --python-platform windows
|
||||||
attrs==23.2.0 ; python_version > '3.11' or sys_platform == 'win32'
|
attrs==23.2.0 ; sys_platform == 'win32' or python_version > '3.11'
|
||||||
# via
|
# via
|
||||||
# outcome
|
# outcome
|
||||||
# trio
|
# trio
|
||||||
cffi==1.16.0 ; implementation_name != 'pypy' and os_name == 'nt' and (python_version > '3.11' or sys_platform == 'win32')
|
cffi==1.16.0 ; (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'win32') or (python_version > '3.11' and implementation_name != 'pypy' and os_name == 'nt')
|
||||||
# via trio
|
# via trio
|
||||||
idna==3.6 ; python_version > '3.11' or sys_platform == 'win32'
|
idna==3.6 ; sys_platform == 'win32' or python_version > '3.11'
|
||||||
# via trio
|
# via trio
|
||||||
outcome==1.3.0.post0 ; python_version > '3.11' or sys_platform == 'win32'
|
outcome==1.3.0.post0 ; sys_platform == 'win32' or python_version > '3.11'
|
||||||
# via trio
|
# via trio
|
||||||
pycparser==2.21 ; implementation_name != 'pypy' and os_name == 'nt' and (python_version > '3.11' or sys_platform == 'win32')
|
pycparser==2.21 ; (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'win32') or (python_version > '3.11' and implementation_name != 'pypy' and os_name == 'nt')
|
||||||
# via cffi
|
# via cffi
|
||||||
sniffio==1.3.1 ; python_version > '3.11' or sys_platform == 'win32'
|
sniffio==1.3.1 ; sys_platform == 'win32' or python_version > '3.11'
|
||||||
# via trio
|
# via trio
|
||||||
sortedcontainers==2.4.0 ; python_version > '3.11' or sys_platform == 'win32'
|
sortedcontainers==2.4.0 ; sys_platform == 'win32' or python_version > '3.11'
|
||||||
# via trio
|
# via trio
|
||||||
trio==0.25.0 ; python_version > '3.11' or sys_platform == 'win32'
|
trio==0.25.0 ; sys_platform == 'win32' or python_version > '3.11'
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
|
|
@ -6618,7 +6618,7 @@ fn universal_conflicting() -> Result<()> {
|
||||||
# via trio
|
# via trio
|
||||||
outcome==1.3.0.post0 ; sys_platform == 'darwin' or sys_platform == 'win32'
|
outcome==1.3.0.post0 ; sys_platform == 'darwin' or sys_platform == 'win32'
|
||||||
# via trio
|
# via trio
|
||||||
pycparser==2.21 ; (sys_platform == 'darwin' or sys_platform == 'win32') and ((implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32'))
|
pycparser==2.21 ; (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32')
|
||||||
# via cffi
|
# via cffi
|
||||||
sniffio==1.3.1 ; sys_platform == 'darwin' or sys_platform == 'win32'
|
sniffio==1.3.1 ; sys_platform == 'darwin' or sys_platform == 'win32'
|
||||||
# via trio
|
# via trio
|
||||||
|
|
@ -7209,27 +7209,27 @@ fn universal_disjoint_base_or_local_requirement() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
|
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
|
||||||
cmake==3.28.4 ; python_version <= '3.12' and python_version >= '3.11' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
cmake==3.28.4 ; python_version >= '3.11' and python_version <= '3.12' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
||||||
# via triton
|
# via triton
|
||||||
. ; python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11')
|
.
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
filelock==3.13.1 ; python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11') or (python_version <= '3.12' and python_version >= '3.11' and platform_machine == 'x86_64' and platform_system == 'Linux')
|
filelock==3.13.1
|
||||||
# via
|
# via
|
||||||
# torch
|
# torch
|
||||||
# triton
|
# triton
|
||||||
jinja2==3.1.3 ; python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11')
|
jinja2==3.1.3
|
||||||
# via torch
|
# via torch
|
||||||
lit==18.1.2 ; python_version <= '3.12' and python_version >= '3.11' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
lit==18.1.2 ; python_version >= '3.11' and python_version <= '3.12' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
||||||
# via triton
|
# via triton
|
||||||
markupsafe==2.1.5 ; (python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11')) and (python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11'))
|
markupsafe==2.1.5
|
||||||
# via jinja2
|
# via jinja2
|
||||||
mpmath==1.3.0 ; (python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11')) and (python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11'))
|
mpmath==1.3.0
|
||||||
# via sympy
|
# via sympy
|
||||||
networkx==3.2.1 ; python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11')
|
networkx==3.2.1
|
||||||
# via torch
|
# via torch
|
||||||
sympy==1.12 ; python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11')
|
sympy==1.12
|
||||||
# via torch
|
# via torch
|
||||||
torch==2.0.0 ; python_version < '3.11' or (python_version <= '3.12' and python_version > '3.12' and python_version >= '3.11')
|
torch==2.0.0 ; python_version < '3.11'
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# example
|
# example
|
||||||
|
|
@ -7237,14 +7237,14 @@ fn universal_disjoint_base_or_local_requirement() -> Result<()> {
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# example
|
# example
|
||||||
torch==2.0.0+cu118 ; python_version <= '3.12' and python_version >= '3.11'
|
torch==2.0.0+cu118 ; python_version >= '3.11' and python_version <= '3.12'
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# example
|
# example
|
||||||
# triton
|
# triton
|
||||||
triton==2.0.0 ; python_version <= '3.12' and python_version >= '3.11' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
triton==2.0.0 ; python_version >= '3.11' and python_version <= '3.12' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
||||||
# via torch
|
# via torch
|
||||||
typing-extensions==4.10.0 ; python_version < '3.11' or python_version > '3.12' or (python_version <= '3.12' and python_version >= '3.11')
|
typing-extensions==4.10.0
|
||||||
# via torch
|
# via torch
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
|
|
@ -7453,7 +7453,7 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> {
|
||||||
# via triton
|
# via triton
|
||||||
. ; os_name == 'Linux'
|
. ; os_name == 'Linux'
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
filelock==3.13.1 ; os_name != 'Linux' or (os_name == 'Linux' and platform_machine == 'x86_64') or (os_name == 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux') or (os_name == 'Linux' and platform_machine != 'x86_64')
|
filelock==3.13.1
|
||||||
# via
|
# via
|
||||||
# torch
|
# torch
|
||||||
# triton
|
# triton
|
||||||
|
|
@ -7461,17 +7461,17 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> {
|
||||||
# via torch
|
# via torch
|
||||||
intel-openmp==2021.4.0 ; os_name != 'Linux' and platform_system == 'Windows'
|
intel-openmp==2021.4.0 ; os_name != 'Linux' and platform_system == 'Windows'
|
||||||
# via mkl
|
# via mkl
|
||||||
jinja2==3.1.3 ; os_name != 'Linux' or (os_name == 'Linux' and platform_machine == 'x86_64') or (os_name == 'Linux' and platform_machine != 'x86_64')
|
jinja2==3.1.3
|
||||||
# via torch
|
# via torch
|
||||||
lit==18.1.2 ; os_name == 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
lit==18.1.2 ; os_name == 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
||||||
# via triton
|
# via triton
|
||||||
markupsafe==2.1.5 ; os_name != 'Linux' or (os_name == 'Linux' and platform_machine == 'x86_64') or (os_name == 'Linux' and platform_machine != 'x86_64')
|
markupsafe==2.1.5
|
||||||
# via jinja2
|
# via jinja2
|
||||||
mkl==2021.4.0 ; os_name != 'Linux' and platform_system == 'Windows'
|
mkl==2021.4.0 ; os_name != 'Linux' and platform_system == 'Windows'
|
||||||
# via torch
|
# via torch
|
||||||
mpmath==1.3.0 ; os_name != 'Linux' or (os_name == 'Linux' and platform_machine == 'x86_64') or (os_name == 'Linux' and platform_machine != 'x86_64')
|
mpmath==1.3.0
|
||||||
# via sympy
|
# via sympy
|
||||||
networkx==3.2.1 ; os_name != 'Linux' or (os_name == 'Linux' and platform_machine == 'x86_64') or (os_name == 'Linux' and platform_machine != 'x86_64')
|
networkx==3.2.1
|
||||||
# via torch
|
# via torch
|
||||||
nvidia-cublas-cu12==12.1.3.1 ; os_name != 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
nvidia-cublas-cu12==12.1.3.1 ; os_name != 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
||||||
# via
|
# via
|
||||||
|
|
@ -7504,7 +7504,7 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> {
|
||||||
# nvidia-cusparse-cu12
|
# nvidia-cusparse-cu12
|
||||||
nvidia-nvtx-cu12==12.1.105 ; os_name != 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
nvidia-nvtx-cu12==12.1.105 ; os_name != 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
||||||
# via torch
|
# via torch
|
||||||
sympy==1.12 ; os_name != 'Linux' or (os_name == 'Linux' and platform_machine == 'x86_64') or (os_name == 'Linux' and platform_machine != 'x86_64')
|
sympy==1.12
|
||||||
# via torch
|
# via torch
|
||||||
tbb==2021.11.0 ; os_name != 'Linux' and platform_system == 'Windows'
|
tbb==2021.11.0 ; os_name != 'Linux' and platform_system == 'Windows'
|
||||||
# via mkl
|
# via mkl
|
||||||
|
|
@ -7521,7 +7521,7 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> {
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
triton==2.0.0 ; os_name == 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
triton==2.0.0 ; os_name == 'Linux' and platform_machine == 'x86_64' and platform_system == 'Linux'
|
||||||
# via torch
|
# via torch
|
||||||
typing-extensions==4.10.0 ; os_name != 'Linux' or (os_name == 'Linux' and platform_machine == 'x86_64') or (os_name == 'Linux' and platform_machine != 'x86_64')
|
typing-extensions==4.10.0
|
||||||
# via torch
|
# via torch
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
|
|
@ -7753,7 +7753,7 @@ fn universal_transitive_disjoint_prerelease_requirement() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
|
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
|
||||||
cffi==1.16.0 ; os_name == 'linux' or platform_python_implementation != 'PyPy'
|
cffi==1.16.0 ; platform_python_implementation != 'PyPy' or os_name == 'linux'
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# cryptography
|
# cryptography
|
||||||
|
|
@ -8141,31 +8141,31 @@ fn universal_prefer_upper_bounds() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.8 --universal
|
# uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.8 --universal
|
||||||
astroid==2.15.8 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin')
|
astroid==2.15.8
|
||||||
# via pylint
|
# via pylint
|
||||||
colorama==0.4.6 ; sys_platform == 'win32'
|
colorama==0.4.6 ; sys_platform == 'win32'
|
||||||
# via pylint
|
# via pylint
|
||||||
dill==0.3.8
|
dill==0.3.8
|
||||||
# via pylint
|
# via pylint
|
||||||
isort==5.13.2 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin')
|
isort==5.13.2
|
||||||
# via pylint
|
# via pylint
|
||||||
lazy-object-proxy==1.10.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin')
|
lazy-object-proxy==1.10.0
|
||||||
# via astroid
|
# via astroid
|
||||||
mccabe==0.7.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin')
|
mccabe==0.7.0
|
||||||
# via pylint
|
# via pylint
|
||||||
platformdirs==4.2.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin')
|
platformdirs==4.2.0
|
||||||
# via pylint
|
# via pylint
|
||||||
pylint==2.17.7
|
pylint==2.17.7
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
tomli==2.0.1 ; python_version < '3.11'
|
tomli==2.0.1 ; python_version < '3.11'
|
||||||
# via pylint
|
# via pylint
|
||||||
tomlkit==0.12.4 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin')
|
tomlkit==0.12.4
|
||||||
# via pylint
|
# via pylint
|
||||||
typing-extensions==4.10.0 ; python_version < '3.11'
|
typing-extensions==4.10.0 ; python_version < '3.11'
|
||||||
# via
|
# via
|
||||||
# astroid
|
# astroid
|
||||||
# pylint
|
# pylint
|
||||||
wrapt==1.16.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin')
|
wrapt==1.16.0
|
||||||
# via astroid
|
# via astroid
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue