3015 lines
108 KiB
Rust
3015 lines
108 KiB
Rust
use std::cmp::Ordering;
|
|
use std::fmt::{self, Display, Formatter};
|
|
use std::ops::{Bound, Deref};
|
|
use std::str::FromStr;
|
|
|
|
use itertools::Itertools;
|
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
|
use uv_normalize::ExtraName;
|
|
use uv_pep440::{Version, VersionParseError, VersionSpecifier};
|
|
use version_ranges::Ranges;
|
|
|
|
use super::algebra::{Edges, NodeId, Variable, INTERNER};
|
|
use super::simplify;
|
|
use crate::cursor::Cursor;
|
|
use crate::marker::lowering::{
|
|
CanonicalMarkerValueExtra, CanonicalMarkerValueString, CanonicalMarkerValueVersion,
|
|
};
|
|
use crate::marker::parse;
|
|
use crate::{
|
|
MarkerEnvironment, Pep508Error, Pep508ErrorSource, Pep508Url, Reporter, TracingReporter,
|
|
};
|
|
|
|
/// Ways in which marker evaluation can fail
|
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
pub enum MarkerWarningKind {
|
|
/// Using an old name from PEP 345 instead of the modern equivalent
|
|
/// <https://peps.python.org/pep-0345/#environment-markers>
|
|
DeprecatedMarkerName,
|
|
/// Doing an operation other than `==` and `!=` on a quoted string with `extra`, such as
|
|
/// `extra > "perf"` or `extra == os_name`
|
|
ExtraInvalidComparison,
|
|
/// Comparing a string valued marker and a string lexicographically, such as `"3.9" > "3.10"`
|
|
LexicographicComparison,
|
|
/// Comparing two markers, such as `os_name != sys_implementation`
|
|
MarkerMarkerComparison,
|
|
/// Failed to parse a PEP 440 version or version specifier, e.g. `>=1<2`
|
|
Pep440Error,
|
|
/// Comparing two strings, such as `"3.9" > "3.10"`
|
|
StringStringComparison,
|
|
}
|
|
|
|
/// Those environment markers with a PEP 440 version as value such as `python_version`
|
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
#[allow(clippy::enum_variant_names)]
|
|
pub enum MarkerValueVersion {
|
|
/// `implementation_version`
|
|
ImplementationVersion,
|
|
/// `python_full_version`
|
|
PythonFullVersion,
|
|
/// `python_version`
|
|
PythonVersion,
|
|
}
|
|
|
|
impl Display for MarkerValueVersion {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::ImplementationVersion => f.write_str("implementation_version"),
|
|
Self::PythonFullVersion => f.write_str("python_full_version"),
|
|
Self::PythonVersion => f.write_str("python_version"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Those environment markers with an arbitrary string as value such as `sys_platform`
|
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
pub enum MarkerValueString {
|
|
/// `implementation_name`
|
|
ImplementationName,
|
|
/// `os_name`
|
|
OsName,
|
|
/// Deprecated `os.name` from <https://peps.python.org/pep-0345/#environment-markers>
|
|
OsNameDeprecated,
|
|
/// `platform_machine`
|
|
PlatformMachine,
|
|
/// Deprecated `platform.machine` from <https://peps.python.org/pep-0345/#environment-markers>
|
|
PlatformMachineDeprecated,
|
|
/// `platform_python_implementation`
|
|
PlatformPythonImplementation,
|
|
/// Deprecated `platform.python_implementation` from <https://peps.python.org/pep-0345/#environment-markers>
|
|
PlatformPythonImplementationDeprecated,
|
|
/// Deprecated `python_implementation` from <https://github.com/pypa/packaging/issues/72>
|
|
PythonImplementationDeprecated,
|
|
/// `platform_release`
|
|
PlatformRelease,
|
|
/// `platform_system`
|
|
PlatformSystem,
|
|
/// `platform_version`
|
|
PlatformVersion,
|
|
/// Deprecated `platform.version` from <https://peps.python.org/pep-0345/#environment-markers>
|
|
PlatformVersionDeprecated,
|
|
/// `sys_platform`
|
|
SysPlatform,
|
|
/// Deprecated `sys.platform` from <https://peps.python.org/pep-0345/#environment-markers>
|
|
SysPlatformDeprecated,
|
|
}
|
|
|
|
impl Display for MarkerValueString {
|
|
/// Normalizes deprecated names to the proper ones
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::ImplementationName => f.write_str("implementation_name"),
|
|
Self::OsName | Self::OsNameDeprecated => f.write_str("os_name"),
|
|
Self::PlatformMachine | Self::PlatformMachineDeprecated => {
|
|
f.write_str("platform_machine")
|
|
}
|
|
Self::PlatformPythonImplementation
|
|
| Self::PlatformPythonImplementationDeprecated
|
|
| Self::PythonImplementationDeprecated => f.write_str("platform_python_implementation"),
|
|
Self::PlatformRelease => f.write_str("platform_release"),
|
|
Self::PlatformSystem => f.write_str("platform_system"),
|
|
Self::PlatformVersion | Self::PlatformVersionDeprecated => {
|
|
f.write_str("platform_version")
|
|
}
|
|
Self::SysPlatform | Self::SysPlatformDeprecated => f.write_str("sys_platform"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One of the predefined environment values
|
|
///
|
|
/// <https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers>
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
pub enum MarkerValue {
|
|
/// Those environment markers with a PEP 440 version as value such as `python_version`
|
|
MarkerEnvVersion(MarkerValueVersion),
|
|
/// Those environment markers with an arbitrary string as value such as `sys_platform`
|
|
MarkerEnvString(MarkerValueString),
|
|
/// `extra`. This one is special because it's a list and not env but user given
|
|
Extra,
|
|
/// Not a constant, but a user given quoted string with a value inside such as '3.8' or "windows"
|
|
QuotedString(String),
|
|
}
|
|
|
|
impl FromStr for MarkerValue {
|
|
type Err = String;
|
|
|
|
/// This is specifically for the reserved values
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let value = match s {
|
|
"implementation_name" => Self::MarkerEnvString(MarkerValueString::ImplementationName),
|
|
"implementation_version" => {
|
|
Self::MarkerEnvVersion(MarkerValueVersion::ImplementationVersion)
|
|
}
|
|
"os_name" => Self::MarkerEnvString(MarkerValueString::OsName),
|
|
"os.name" => Self::MarkerEnvString(MarkerValueString::OsNameDeprecated),
|
|
"platform_machine" => Self::MarkerEnvString(MarkerValueString::PlatformMachine),
|
|
"platform.machine" => {
|
|
Self::MarkerEnvString(MarkerValueString::PlatformMachineDeprecated)
|
|
}
|
|
"platform_python_implementation" => {
|
|
Self::MarkerEnvString(MarkerValueString::PlatformPythonImplementation)
|
|
}
|
|
"platform.python_implementation" => {
|
|
Self::MarkerEnvString(MarkerValueString::PlatformPythonImplementationDeprecated)
|
|
}
|
|
"python_implementation" => {
|
|
Self::MarkerEnvString(MarkerValueString::PythonImplementationDeprecated)
|
|
}
|
|
"platform_release" => Self::MarkerEnvString(MarkerValueString::PlatformRelease),
|
|
"platform_system" => Self::MarkerEnvString(MarkerValueString::PlatformSystem),
|
|
"platform_version" => Self::MarkerEnvString(MarkerValueString::PlatformVersion),
|
|
"platform.version" => {
|
|
Self::MarkerEnvString(MarkerValueString::PlatformVersionDeprecated)
|
|
}
|
|
"python_full_version" => Self::MarkerEnvVersion(MarkerValueVersion::PythonFullVersion),
|
|
"python_version" => Self::MarkerEnvVersion(MarkerValueVersion::PythonVersion),
|
|
"sys_platform" => Self::MarkerEnvString(MarkerValueString::SysPlatform),
|
|
"sys.platform" => Self::MarkerEnvString(MarkerValueString::SysPlatformDeprecated),
|
|
"extra" => Self::Extra,
|
|
_ => return Err(format!("Invalid key: {s}")),
|
|
};
|
|
Ok(value)
|
|
}
|
|
}
|
|
|
|
impl Display for MarkerValue {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::MarkerEnvVersion(marker_value_version) => marker_value_version.fmt(f),
|
|
Self::MarkerEnvString(marker_value_string) => marker_value_string.fmt(f),
|
|
Self::Extra => f.write_str("extra"),
|
|
Self::QuotedString(value) => write!(f, "'{value}'"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// How to compare key and value, such as by `==`, `>` or `not in`
|
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
pub enum MarkerOperator {
|
|
/// `==`
|
|
Equal,
|
|
/// `!=`
|
|
NotEqual,
|
|
/// `>`
|
|
GreaterThan,
|
|
/// `>=`
|
|
GreaterEqual,
|
|
/// `<`
|
|
LessThan,
|
|
/// `<=`
|
|
LessEqual,
|
|
/// `~=`
|
|
TildeEqual,
|
|
/// `in`
|
|
In,
|
|
/// `not in`
|
|
NotIn,
|
|
/// The inverse of the `in` operator.
|
|
///
|
|
/// This is not a valid operator when parsing but is used for normalizing
|
|
/// marker trees.
|
|
Contains,
|
|
/// The inverse of the `not in` operator.
|
|
///
|
|
/// This is not a valid operator when parsing but is used for normalizing
|
|
/// marker trees.
|
|
NotContains,
|
|
}
|
|
|
|
impl MarkerOperator {
|
|
/// Compare two versions, returning `None` for `in` and `not in`.
|
|
pub(crate) fn to_pep440_operator(self) -> Option<uv_pep440::Operator> {
|
|
match self {
|
|
Self::Equal => Some(uv_pep440::Operator::Equal),
|
|
Self::NotEqual => Some(uv_pep440::Operator::NotEqual),
|
|
Self::GreaterThan => Some(uv_pep440::Operator::GreaterThan),
|
|
Self::GreaterEqual => Some(uv_pep440::Operator::GreaterThanEqual),
|
|
Self::LessThan => Some(uv_pep440::Operator::LessThan),
|
|
Self::LessEqual => Some(uv_pep440::Operator::LessThanEqual),
|
|
Self::TildeEqual => Some(uv_pep440::Operator::TildeEqual),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Inverts this marker operator.
|
|
pub(crate) fn invert(self) -> MarkerOperator {
|
|
match self {
|
|
Self::LessThan => Self::GreaterThan,
|
|
Self::LessEqual => Self::GreaterEqual,
|
|
Self::GreaterThan => Self::LessThan,
|
|
Self::GreaterEqual => Self::LessEqual,
|
|
Self::Equal => Self::Equal,
|
|
Self::NotEqual => Self::NotEqual,
|
|
Self::TildeEqual => Self::TildeEqual,
|
|
Self::In => Self::Contains,
|
|
Self::NotIn => Self::NotContains,
|
|
Self::Contains => Self::In,
|
|
Self::NotContains => Self::NotIn,
|
|
}
|
|
}
|
|
|
|
/// Negates this marker operator.
|
|
///
|
|
/// If a negation doesn't exist, which is only the case for ~=, then this
|
|
/// returns `None`.
|
|
pub(crate) fn negate(self) -> Option<MarkerOperator> {
|
|
Some(match self {
|
|
Self::Equal => Self::NotEqual,
|
|
Self::NotEqual => Self::Equal,
|
|
Self::TildeEqual => return None,
|
|
Self::LessThan => Self::GreaterEqual,
|
|
Self::LessEqual => Self::GreaterThan,
|
|
Self::GreaterThan => Self::LessEqual,
|
|
Self::GreaterEqual => Self::LessThan,
|
|
Self::In => Self::NotIn,
|
|
Self::NotIn => Self::In,
|
|
Self::Contains => Self::NotContains,
|
|
Self::NotContains => Self::Contains,
|
|
})
|
|
}
|
|
|
|
/// Returns the marker operator and value whose union represents the given range.
|
|
pub fn from_bounds(
|
|
bounds: (&Bound<String>, &Bound<String>),
|
|
) -> impl Iterator<Item = (MarkerOperator, String)> {
|
|
let (b1, b2) = match bounds {
|
|
(Bound::Included(v1), Bound::Included(v2)) if v1 == v2 => {
|
|
(Some((MarkerOperator::Equal, v1.clone())), None)
|
|
}
|
|
(Bound::Excluded(v1), Bound::Excluded(v2)) if v1 == v2 => {
|
|
(Some((MarkerOperator::NotEqual, v1.clone())), None)
|
|
}
|
|
(lower, upper) => (
|
|
MarkerOperator::from_lower_bound(lower),
|
|
MarkerOperator::from_upper_bound(upper),
|
|
),
|
|
};
|
|
|
|
b1.into_iter().chain(b2)
|
|
}
|
|
|
|
/// Returns a value specifier representing the given lower bound.
|
|
pub fn from_lower_bound(bound: &Bound<String>) -> Option<(MarkerOperator, String)> {
|
|
match bound {
|
|
Bound::Included(value) => Some((MarkerOperator::GreaterEqual, value.clone())),
|
|
Bound::Excluded(value) => Some((MarkerOperator::GreaterThan, value.clone())),
|
|
Bound::Unbounded => None,
|
|
}
|
|
}
|
|
|
|
/// Returns a value specifier representing the given upper bound.
|
|
pub fn from_upper_bound(bound: &Bound<String>) -> Option<(MarkerOperator, String)> {
|
|
match bound {
|
|
Bound::Included(value) => Some((MarkerOperator::LessEqual, value.clone())),
|
|
Bound::Excluded(value) => Some((MarkerOperator::LessThan, value.clone())),
|
|
Bound::Unbounded => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for MarkerOperator {
|
|
type Err = String;
|
|
|
|
/// PEP 508 allows arbitrary whitespace between "not" and "in", and so do we
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let value = match s {
|
|
"==" => Self::Equal,
|
|
"!=" => Self::NotEqual,
|
|
">" => Self::GreaterThan,
|
|
">=" => Self::GreaterEqual,
|
|
"<" => Self::LessThan,
|
|
"<=" => Self::LessEqual,
|
|
"~=" => Self::TildeEqual,
|
|
"in" => Self::In,
|
|
not_space_in
|
|
if not_space_in
|
|
// start with not
|
|
.strip_prefix("not")
|
|
// ends with in
|
|
.and_then(|space_in| space_in.strip_suffix("in"))
|
|
// and has only whitespace in between
|
|
.is_some_and(|space| !space.is_empty() && space.trim().is_empty()) =>
|
|
{
|
|
Self::NotIn
|
|
}
|
|
other => return Err(format!("Invalid comparator: {other}")),
|
|
};
|
|
Ok(value)
|
|
}
|
|
}
|
|
|
|
impl Display for MarkerOperator {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(match self {
|
|
Self::Equal => "==",
|
|
Self::NotEqual => "!=",
|
|
Self::GreaterThan => ">",
|
|
Self::GreaterEqual => ">=",
|
|
Self::LessThan => "<",
|
|
Self::LessEqual => "<=",
|
|
Self::TildeEqual => "~=",
|
|
Self::In | Self::Contains => "in",
|
|
Self::NotIn | Self::NotContains => "not in",
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Helper type with a [Version] and its original text
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub struct StringVersion {
|
|
/// Original unchanged string
|
|
pub string: String,
|
|
/// Parsed version
|
|
pub version: Version,
|
|
}
|
|
|
|
impl From<Version> for StringVersion {
|
|
fn from(version: Version) -> Self {
|
|
Self {
|
|
string: version.to_string(),
|
|
version,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for StringVersion {
|
|
type Err = VersionParseError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(Self {
|
|
string: s.to_string(),
|
|
version: Version::from_str(s)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Display for StringVersion {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
self.string.fmt(f)
|
|
}
|
|
}
|
|
|
|
impl Serialize for StringVersion {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_str(&self.string)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for StringVersion {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let string = String::deserialize(deserializer)?;
|
|
Self::from_str(&string).map_err(de::Error::custom)
|
|
}
|
|
}
|
|
|
|
impl Deref for StringVersion {
|
|
type Target = Version;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.version
|
|
}
|
|
}
|
|
|
|
/// The [`ExtraName`] value used in `extra` markers.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
pub enum MarkerValueExtra {
|
|
/// A valid [`ExtraName`].
|
|
Extra(ExtraName),
|
|
/// An invalid name, preserved as an arbitrary string.
|
|
Arbitrary(String),
|
|
}
|
|
|
|
impl MarkerValueExtra {
|
|
/// Returns the [`ExtraName`] for this value, if it is a valid extra.
|
|
pub fn as_extra(&self) -> Option<&ExtraName> {
|
|
match self {
|
|
Self::Extra(extra) => Some(extra),
|
|
Self::Arbitrary(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Convert the [`MarkerValueExtra`] to an [`ExtraName`], if possible.
|
|
fn into_extra(self) -> Option<ExtraName> {
|
|
match self {
|
|
Self::Extra(extra) => Some(extra),
|
|
Self::Arbitrary(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for MarkerValueExtra {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Extra(extra) => extra.fmt(f),
|
|
Self::Arbitrary(string) => string.fmt(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents one clause such as `python_version > "3.8"`.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
#[allow(missing_docs)]
|
|
pub enum MarkerExpression {
|
|
/// A version expression, e.g. `<version key> <version op> <quoted PEP 440 version>`.
|
|
///
|
|
/// Inverted version expressions, such as `<version> <version op> <version key>`, are also
|
|
/// normalized to this form.
|
|
Version {
|
|
key: MarkerValueVersion,
|
|
specifier: VersionSpecifier,
|
|
},
|
|
/// A version in list expression, e.g. `<version key> in <quoted list of PEP 440 versions>`.
|
|
///
|
|
/// A special case of [`MarkerExpression::String`] with the [`MarkerOperator::In`] operator for
|
|
/// [`MarkerValueVersion`] values.
|
|
///
|
|
/// See [`parse::parse_version_in_expr`] for details on the supported syntax.
|
|
///
|
|
/// Negated expressions, using "not in" are represented using `negated = true`.
|
|
VersionIn {
|
|
key: MarkerValueVersion,
|
|
versions: Vec<Version>,
|
|
negated: bool,
|
|
},
|
|
/// An string marker comparison, e.g. `sys_platform == '...'`.
|
|
///
|
|
/// Inverted string expressions, e.g `'...' == sys_platform`, are also normalized to this form.
|
|
String {
|
|
key: MarkerValueString,
|
|
operator: MarkerOperator,
|
|
value: String,
|
|
},
|
|
/// `extra <extra op> '...'` or `'...' <extra op> extra`.
|
|
Extra {
|
|
operator: ExtraOperator,
|
|
name: MarkerValueExtra,
|
|
},
|
|
}
|
|
|
|
/// The operator for an extra expression, either '==' or '!='.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
pub enum ExtraOperator {
|
|
/// `==`
|
|
Equal,
|
|
/// `!=`
|
|
NotEqual,
|
|
}
|
|
|
|
impl ExtraOperator {
|
|
/// Creates a [`ExtraOperator`] from an equivalent [`MarkerOperator`].
|
|
///
|
|
/// Returns `None` if the operator is not supported for extras.
|
|
pub(crate) fn from_marker_operator(operator: MarkerOperator) -> Option<ExtraOperator> {
|
|
match operator {
|
|
MarkerOperator::Equal => Some(ExtraOperator::Equal),
|
|
MarkerOperator::NotEqual => Some(ExtraOperator::NotEqual),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Negates this operator.
|
|
pub(crate) fn negate(&self) -> ExtraOperator {
|
|
match *self {
|
|
ExtraOperator::Equal => ExtraOperator::NotEqual,
|
|
ExtraOperator::NotEqual => ExtraOperator::Equal,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for ExtraOperator {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(match self {
|
|
Self::Equal => "==",
|
|
Self::NotEqual => "!=",
|
|
})
|
|
}
|
|
}
|
|
|
|
impl MarkerExpression {
|
|
/// Parse a [`MarkerExpression`] from a string with the given reporter.
|
|
pub fn parse_reporter(
|
|
s: &str,
|
|
reporter: &mut impl Reporter,
|
|
) -> Result<Option<Self>, Pep508Error> {
|
|
let mut chars = Cursor::new(s);
|
|
let expression = parse::parse_marker_key_op_value(&mut chars, reporter)?;
|
|
chars.eat_whitespace();
|
|
if let Some((pos, unexpected)) = chars.next() {
|
|
return Err(Pep508Error {
|
|
message: Pep508ErrorSource::String(format!(
|
|
"Unexpected character '{unexpected}', expected end of input"
|
|
)),
|
|
start: pos,
|
|
len: chars.remaining(),
|
|
input: chars.to_string(),
|
|
});
|
|
}
|
|
|
|
Ok(expression)
|
|
}
|
|
|
|
/// Parse a [`MarkerExpression`] from a string.
|
|
///
|
|
/// Returns `None` if the expression consists entirely of meaningless expressions
|
|
/// that are ignored, such as `os_name ~= 'foo'`.
|
|
#[allow(clippy::should_implement_trait)]
|
|
pub fn from_str(s: &str) -> Result<Option<Self>, Pep508Error> {
|
|
MarkerExpression::parse_reporter(s, &mut TracingReporter)
|
|
}
|
|
}
|
|
|
|
impl Display for MarkerExpression {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
MarkerExpression::Version { key, specifier } => {
|
|
let (op, version) = (specifier.operator(), specifier.version());
|
|
if op == &uv_pep440::Operator::EqualStar || op == &uv_pep440::Operator::NotEqualStar
|
|
{
|
|
return write!(f, "{key} {op} '{version}.*'");
|
|
}
|
|
write!(f, "{key} {op} '{version}'")
|
|
}
|
|
MarkerExpression::VersionIn {
|
|
key,
|
|
versions,
|
|
negated,
|
|
} => {
|
|
let op = if *negated { "not in" } else { "in" };
|
|
let versions = versions.iter().map(ToString::to_string).join(" ");
|
|
write!(f, "{key} {op} '{versions}'")
|
|
}
|
|
MarkerExpression::String {
|
|
key,
|
|
operator,
|
|
value,
|
|
} => {
|
|
if matches!(
|
|
operator,
|
|
MarkerOperator::Contains | MarkerOperator::NotContains
|
|
) {
|
|
return write!(f, "'{value}' {} {key}", operator.invert());
|
|
}
|
|
|
|
write!(f, "{key} {operator} '{value}'")
|
|
}
|
|
MarkerExpression::Extra { operator, name } => {
|
|
write!(f, "extra {operator} '{name}'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents one or more nested marker expressions with and/or/parentheses.
|
|
///
|
|
/// Marker trees are canonical, meaning any two functionally equivalent markers
|
|
/// will compare equally. Markers also support efficient polynomial-time operations,
|
|
/// such as conjunction and disjunction.
|
|
// TODO(ibraheem): decide whether we want to implement `Copy` for marker trees
|
|
#[derive(Clone, Eq, Hash, PartialEq)]
|
|
pub struct MarkerTree(NodeId);
|
|
|
|
impl Default for MarkerTree {
|
|
fn default() -> Self {
|
|
MarkerTree::TRUE
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for MarkerTree {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
FromStr::from_str(&s).map_err(de::Error::custom)
|
|
}
|
|
}
|
|
|
|
impl FromStr for MarkerTree {
|
|
type Err = Pep508Error;
|
|
|
|
fn from_str(markers: &str) -> Result<Self, Self::Err> {
|
|
parse::parse_markers(markers, &mut TracingReporter)
|
|
}
|
|
}
|
|
|
|
impl MarkerTree {
|
|
/// Like [`FromStr::from_str`], but the caller chooses the return type generic.
|
|
pub fn parse_str<T: Pep508Url>(markers: &str) -> Result<Self, Pep508Error<T>> {
|
|
parse::parse_markers(markers, &mut TracingReporter)
|
|
}
|
|
|
|
/// Parse a [`MarkerTree`] from a string with the given reporter.
|
|
pub fn parse_reporter(
|
|
markers: &str,
|
|
reporter: &mut impl Reporter,
|
|
) -> Result<Self, Pep508Error> {
|
|
parse::parse_markers(markers, reporter)
|
|
}
|
|
|
|
/// An empty marker that always evaluates to `true`.
|
|
pub const TRUE: MarkerTree = MarkerTree(NodeId::TRUE);
|
|
|
|
/// An unsatisfiable marker that always evaluates to `false`.
|
|
pub const FALSE: MarkerTree = MarkerTree(NodeId::FALSE);
|
|
|
|
/// Returns a marker tree for a single expression.
|
|
pub fn expression(expr: MarkerExpression) -> MarkerTree {
|
|
MarkerTree(INTERNER.lock().expression(expr))
|
|
}
|
|
|
|
/// Whether the marker always evaluates to `true`.
|
|
///
|
|
/// If this method returns `true`, it is definitively known that the marker will
|
|
/// evaluate to `true` in any environment. However, this method may return false
|
|
/// negatives, i.e. it may not be able to detect that a marker is always true for
|
|
/// complex expressions.
|
|
pub fn is_true(&self) -> bool {
|
|
self.0.is_true()
|
|
}
|
|
|
|
/// Whether the marker always evaluates to `false`, i.e. the expression is not
|
|
/// satisfiable in any environment.
|
|
///
|
|
/// If this method returns `true`, it is definitively known that the marker will
|
|
/// evaluate to `false` in any environment. However, this method may return false
|
|
/// negatives, i.e. it may not be able to detect that a marker is unsatisfiable
|
|
/// for complex expressions.
|
|
pub fn is_false(&self) -> bool {
|
|
self.0.is_false()
|
|
}
|
|
|
|
/// Returns a new marker tree that is the negation of this one.
|
|
#[must_use]
|
|
pub fn negate(&self) -> MarkerTree {
|
|
MarkerTree(self.0.not())
|
|
}
|
|
|
|
/// Combine this marker tree with the one given via a conjunction.
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn and(&mut self, tree: MarkerTree) {
|
|
self.0 = INTERNER.lock().and(self.0, tree.0);
|
|
}
|
|
|
|
/// Combine this marker tree with the one given via a disjunction.
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn or(&mut self, tree: MarkerTree) {
|
|
self.0 = INTERNER.lock().or(self.0, tree.0);
|
|
}
|
|
|
|
/// Sets this to a marker equivalent to the implication of this one and the
|
|
/// given consequent.
|
|
///
|
|
/// If the marker set is always `true`, then it can be said that `self`
|
|
/// implies `consequent`.
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn implies(&mut self, consequent: MarkerTree) {
|
|
// This could probably be optimized, but is clearly
|
|
// correct, since logical implication is `-P or Q`.
|
|
*self = self.negate();
|
|
self.or(consequent);
|
|
}
|
|
|
|
/// Returns `true` if there is no environment in which both marker trees can apply,
|
|
/// i.e. their conjunction is always `false`.
|
|
///
|
|
/// If this method returns `true`, it is definitively known that the two markers can
|
|
/// never both evaluate to `true` in a given environment. However, this method may return
|
|
/// false negatives, i.e. it may not be able to detect that two markers are disjoint for
|
|
/// complex expressions.
|
|
pub fn is_disjoint(&self, other: &MarkerTree) -> bool {
|
|
INTERNER.lock().is_disjoint(self.0, other.0)
|
|
}
|
|
|
|
/// Returns the contents of this marker tree, if it contains at least one expression.
|
|
///
|
|
/// If the marker is `true`, this method will return `None`.
|
|
/// If the marker is `false`, the marker is represented as the normalized expression, `python_version < '0'`.
|
|
///
|
|
/// The returned type implements [`Display`] and [`serde::Serialize`].
|
|
pub fn contents(&self) -> Option<MarkerTreeContents> {
|
|
if self.is_true() {
|
|
return None;
|
|
}
|
|
|
|
Some(MarkerTreeContents(self.clone()))
|
|
}
|
|
|
|
/// Returns a simplified string representation of this marker, if it contains at least one
|
|
/// expression.
|
|
///
|
|
/// If the marker is `true`, this method will return `None`.
|
|
/// If the marker is `false`, the marker is represented as the normalized expression, `python_version < '0'`.
|
|
pub fn try_to_string(&self) -> Option<String> {
|
|
self.contents().map(|contents| contents.to_string())
|
|
}
|
|
|
|
/// Returns the underlying [`MarkerTreeKind`] of the root node.
|
|
pub fn kind(&self) -> MarkerTreeKind<'_> {
|
|
if self.is_true() {
|
|
return MarkerTreeKind::True;
|
|
}
|
|
|
|
if self.is_false() {
|
|
return MarkerTreeKind::False;
|
|
}
|
|
|
|
let node = INTERNER.shared.node(self.0);
|
|
match &node.var {
|
|
Variable::Version(key) => {
|
|
let Edges::Version { edges: ref map } = node.children else {
|
|
unreachable!()
|
|
};
|
|
MarkerTreeKind::Version(VersionMarkerTree {
|
|
id: self.0,
|
|
key: *key,
|
|
map,
|
|
})
|
|
}
|
|
Variable::String(key) => {
|
|
let Edges::String { edges: ref map } = node.children else {
|
|
unreachable!()
|
|
};
|
|
MarkerTreeKind::String(StringMarkerTree {
|
|
id: self.0,
|
|
key: *key,
|
|
map,
|
|
})
|
|
}
|
|
Variable::In { key, value } => {
|
|
let Edges::Boolean { low, high } = node.children else {
|
|
unreachable!()
|
|
};
|
|
MarkerTreeKind::In(InMarkerTree {
|
|
key: *key,
|
|
value,
|
|
high: high.negate(self.0),
|
|
low: low.negate(self.0),
|
|
})
|
|
}
|
|
Variable::Contains { key, value } => {
|
|
let Edges::Boolean { low, high } = node.children else {
|
|
unreachable!()
|
|
};
|
|
MarkerTreeKind::Contains(ContainsMarkerTree {
|
|
key: *key,
|
|
value,
|
|
high: high.negate(self.0),
|
|
low: low.negate(self.0),
|
|
})
|
|
}
|
|
Variable::Extra(name) => {
|
|
let Edges::Boolean { low, high } = node.children else {
|
|
unreachable!()
|
|
};
|
|
MarkerTreeKind::Extra(ExtraMarkerTree {
|
|
name,
|
|
high: high.negate(self.0),
|
|
low: low.negate(self.0),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a simplified DNF expression for this marker tree.
|
|
pub fn to_dnf(&self) -> Vec<Vec<MarkerExpression>> {
|
|
simplify::to_dnf(self)
|
|
}
|
|
|
|
/// Does this marker apply in the given environment?
|
|
pub fn evaluate(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
|
|
self.evaluate_reporter_impl(env, extras, &mut TracingReporter)
|
|
}
|
|
|
|
/// Evaluates this marker tree against an optional environment and a
|
|
/// possibly empty sequence of extras.
|
|
///
|
|
/// When an environment is not provided, all marker expressions based on
|
|
/// the environment evaluate to `true`. That is, this provides environment
|
|
/// independent marker evaluation. In practice, this means only the extras
|
|
/// are evaluated when an environment is not provided.
|
|
pub fn evaluate_optional_environment(
|
|
&self,
|
|
env: Option<&MarkerEnvironment>,
|
|
extras: &[ExtraName],
|
|
) -> bool {
|
|
match env {
|
|
None => self.evaluate_extras(extras),
|
|
Some(env) => self.evaluate_reporter_impl(env, extras, &mut TracingReporter),
|
|
}
|
|
}
|
|
|
|
/// Same as [`Self::evaluate`], but instead of using logging to warn, you can pass your own
|
|
/// handler for warnings
|
|
pub fn evaluate_reporter(
|
|
&self,
|
|
env: &MarkerEnvironment,
|
|
extras: &[ExtraName],
|
|
reporter: &mut impl Reporter,
|
|
) -> bool {
|
|
self.evaluate_reporter_impl(env, extras, reporter)
|
|
}
|
|
|
|
fn evaluate_reporter_impl(
|
|
&self,
|
|
env: &MarkerEnvironment,
|
|
extras: &[ExtraName],
|
|
reporter: &mut impl Reporter,
|
|
) -> bool {
|
|
match self.kind() {
|
|
MarkerTreeKind::True => return true,
|
|
MarkerTreeKind::False => return false,
|
|
MarkerTreeKind::Version(marker) => {
|
|
for (range, tree) in marker.edges() {
|
|
if range.contains(env.get_version(marker.key())) {
|
|
return tree.evaluate_reporter_impl(env, extras, reporter);
|
|
}
|
|
}
|
|
}
|
|
MarkerTreeKind::String(marker) => {
|
|
for (range, tree) in marker.children() {
|
|
let l_string = env.get_string(marker.key());
|
|
|
|
if range.as_singleton().is_none() {
|
|
if let Some((start, end)) = range.bounding_range() {
|
|
if let Bound::Included(value) | Bound::Excluded(value) = start {
|
|
reporter.report(
|
|
MarkerWarningKind::LexicographicComparison,
|
|
format!("Comparing {l_string} and {value} lexicographically"),
|
|
);
|
|
};
|
|
|
|
if let Bound::Included(value) | Bound::Excluded(value) = end {
|
|
reporter.report(
|
|
MarkerWarningKind::LexicographicComparison,
|
|
format!("Comparing {l_string} and {value} lexicographically"),
|
|
);
|
|
};
|
|
}
|
|
}
|
|
|
|
// todo(ibraheem): avoid cloning here, `contains` should accept `&impl Borrow<V>`
|
|
let l_string = &l_string.to_string();
|
|
if range.contains(l_string) {
|
|
return tree.evaluate_reporter_impl(env, extras, reporter);
|
|
}
|
|
}
|
|
}
|
|
MarkerTreeKind::In(marker) => {
|
|
return marker
|
|
.edge(marker.value().contains(env.get_string(marker.key())))
|
|
.evaluate_reporter_impl(env, extras, reporter);
|
|
}
|
|
MarkerTreeKind::Contains(marker) => {
|
|
return marker
|
|
.edge(env.get_string(marker.key()).contains(marker.value()))
|
|
.evaluate_reporter_impl(env, extras, reporter);
|
|
}
|
|
MarkerTreeKind::Extra(marker) => {
|
|
return marker
|
|
.edge(extras.contains(marker.name().extra()))
|
|
.evaluate_reporter_impl(env, extras, reporter);
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Checks if the requirement should be activated with the given set of active extras without evaluating
|
|
/// the remaining environment markers, i.e. if there is potentially an environment that could activate this
|
|
/// requirement.
|
|
pub fn evaluate_extras(&self, extras: &[ExtraName]) -> bool {
|
|
match self.kind() {
|
|
MarkerTreeKind::True => true,
|
|
MarkerTreeKind::False => false,
|
|
MarkerTreeKind::Version(marker) => {
|
|
marker.edges().any(|(_, tree)| tree.evaluate_extras(extras))
|
|
}
|
|
MarkerTreeKind::String(marker) => marker
|
|
.children()
|
|
.any(|(_, tree)| tree.evaluate_extras(extras)),
|
|
MarkerTreeKind::In(marker) => marker
|
|
.children()
|
|
.any(|(_, tree)| tree.evaluate_extras(extras)),
|
|
MarkerTreeKind::Contains(marker) => marker
|
|
.children()
|
|
.any(|(_, tree)| tree.evaluate_extras(extras)),
|
|
MarkerTreeKind::Extra(marker) => marker
|
|
.edge(extras.contains(marker.name().extra()))
|
|
.evaluate_extras(extras),
|
|
}
|
|
}
|
|
|
|
/// Find a top level `extra == "..."` expression.
|
|
///
|
|
/// ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part of the
|
|
/// main conjunction.
|
|
pub fn top_level_extra(&self) -> Option<MarkerExpression> {
|
|
let mut extra_expression = None;
|
|
for conjunction in self.to_dnf() {
|
|
let found = conjunction.iter().find(|expression| {
|
|
matches!(
|
|
expression,
|
|
MarkerExpression::Extra {
|
|
operator: ExtraOperator::Equal,
|
|
..
|
|
}
|
|
)
|
|
})?;
|
|
|
|
// Because the marker tree is in DNF form, we must verify that the extra expression is part
|
|
// of all solutions to this marker.
|
|
if let Some(ref extra_expression) = extra_expression {
|
|
if *extra_expression != *found {
|
|
return None;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
extra_expression = Some(found.clone());
|
|
}
|
|
|
|
extra_expression
|
|
}
|
|
|
|
/// Find a top level `extra == "..."` name.
|
|
///
|
|
/// ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part of the
|
|
/// main conjunction.
|
|
pub fn top_level_extra_name(&self) -> Option<ExtraName> {
|
|
let extra_expression = self.top_level_extra()?;
|
|
|
|
match extra_expression {
|
|
MarkerExpression::Extra { name, .. } => name.into_extra(),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Simplify this marker by *assuming* that the Python version range
|
|
/// provided is true and that the complement of it is false.
|
|
///
|
|
/// For example, with `requires-python = '>=3.8'` and a marker tree of
|
|
/// `python_full_version >= '3.8' and python_full_version <= '3.10'`, this
|
|
/// would result in a marker of `python_full_version <= '3.10'`.
|
|
///
|
|
/// This is useful when one wants to write "simpler" markers in a
|
|
/// particular context with a bound on the supported Python versions.
|
|
/// In general, the simplified markers returned shouldn't be used for
|
|
/// evaluation. Instead, they should be turned back into their more
|
|
/// "complex" form first.
|
|
///
|
|
/// Note that simplifying a marker and then complexifying it, even
|
|
/// with the same Python version bounds, is a lossy operation. For
|
|
/// example, simplifying `python_version < '3.7'` with `requires-python
|
|
/// = ">=3.8"` will result in a marker that always returns false (e.g.,
|
|
/// `python_version < '0'`). Therefore, complexifying an always-false
|
|
/// marker will result in a marker that is still always false, despite
|
|
/// the fact that the original marker was true for `<3.7`. Therefore,
|
|
/// simplifying should only be done as a one-way transformation when it is
|
|
/// known that `requires-python` reflects an eternal lower bound on the
|
|
/// results of that simplification. (If `requires-python` changes, then one
|
|
/// should reconstitute all relevant markers from the source data.)
|
|
#[must_use]
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn simplify_python_versions(
|
|
self,
|
|
lower: Bound<&Version>,
|
|
upper: Bound<&Version>,
|
|
) -> MarkerTree {
|
|
MarkerTree(
|
|
INTERNER
|
|
.lock()
|
|
.simplify_python_versions(self.0, lower, upper),
|
|
)
|
|
}
|
|
|
|
/// Complexify marker tree by requiring the given Python version range
|
|
/// to be true in order for this marker tree to evaluate to true in all
|
|
/// circumstances.
|
|
///
|
|
/// For example, with `requires-python = '>=3.8'` and a marker tree of
|
|
/// `python_full_version <= '3.10'`, this would result in a marker of
|
|
/// `python_full_version >= '3.8' and python_full_version <= '3.10'`.
|
|
#[must_use]
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn complexify_python_versions(
|
|
self,
|
|
lower: Bound<&Version>,
|
|
upper: Bound<&Version>,
|
|
) -> MarkerTree {
|
|
MarkerTree(
|
|
INTERNER
|
|
.lock()
|
|
.complexify_python_versions(self.0, lower, upper),
|
|
)
|
|
}
|
|
|
|
/// Remove the extras from a marker, returning `None` if the marker tree evaluates to `true`.
|
|
///
|
|
/// Any `extra` markers that are always `true` given the provided extras will be removed.
|
|
/// Any `extra` markers that are always `false` given the provided extras will be left
|
|
/// unchanged.
|
|
///
|
|
/// For example, if `dev` is a provided extra, given `sys_platform == 'linux' and extra == 'dev'`,
|
|
/// the marker will be simplified to `sys_platform == 'linux'`.
|
|
#[must_use]
|
|
pub fn simplify_extras(self, extras: &[ExtraName]) -> MarkerTree {
|
|
self.simplify_extras_with(|name| extras.contains(name))
|
|
}
|
|
|
|
/// Remove the extras from a marker, returning `None` if the marker tree evaluates to `true`.
|
|
///
|
|
/// Any `extra` markers that are always `true` given the provided predicate will be removed.
|
|
/// Any `extra` markers that are always `false` given the provided predicate will be left
|
|
/// unchanged.
|
|
///
|
|
/// For example, if `is_extra('dev')` is true, given
|
|
/// `sys_platform == 'linux' and extra == 'dev'`, the marker will be simplified to
|
|
/// `sys_platform == 'linux'`.
|
|
#[must_use]
|
|
pub fn simplify_extras_with(self, is_extra: impl Fn(&ExtraName) -> bool) -> MarkerTree {
|
|
// Because `simplify_extras_with_impl` is recursive, and we need to use
|
|
// our predicate in recursive calls, we need the predicate itself to
|
|
// have some indirection (or else we'd have to clone it). To avoid a
|
|
// recursive type at codegen time, we just introduce the indirection
|
|
// here, but keep the calling API ergonomic.
|
|
self.simplify_extras_with_impl(&is_extra)
|
|
}
|
|
|
|
fn simplify_extras_with_impl(self, is_extra: &impl Fn(&ExtraName) -> bool) -> MarkerTree {
|
|
MarkerTree(INTERNER.lock().restrict(self.0, &|var| match var {
|
|
Variable::Extra(name) => is_extra(name.extra()).then_some(true),
|
|
_ => None,
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for MarkerTree {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
if self.is_true() {
|
|
return write!(f, "true");
|
|
}
|
|
if self.is_false() {
|
|
return write!(f, "false");
|
|
}
|
|
write!(f, "{}", self.contents().unwrap())
|
|
}
|
|
}
|
|
|
|
impl MarkerTree {
|
|
/// Formats a [`MarkerTree`] as a graph.
|
|
///
|
|
/// This is useful for debugging when one wants to look at a
|
|
/// representation of a `MarkerTree` that is more faithful to its
|
|
/// internal representation.
|
|
pub fn debug_graph(&self) -> MarkerTreeDebugGraph<'_> {
|
|
MarkerTreeDebugGraph { marker: self }
|
|
}
|
|
|
|
/// Formats a [`MarkerTree`] in its "raw" representation.
|
|
///
|
|
/// This is useful for debugging when one wants to look at a
|
|
/// representation of a `MarkerTree` that is precisely identical
|
|
/// to its internal representation.
|
|
pub fn debug_raw(&self) -> MarkerTreeDebugRaw<'_> {
|
|
MarkerTreeDebugRaw { marker: self }
|
|
}
|
|
|
|
fn fmt_graph(&self, f: &mut fmt::Formatter<'_>, level: usize) -> fmt::Result {
|
|
match self.kind() {
|
|
MarkerTreeKind::True => return write!(f, "true"),
|
|
MarkerTreeKind::False => return write!(f, "false"),
|
|
MarkerTreeKind::Version(kind) => {
|
|
for (tree, range) in simplify::collect_edges(kind.edges()) {
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{key}{range} -> ", key = kind.key())?;
|
|
tree.fmt_graph(f, level + 1)?;
|
|
}
|
|
}
|
|
MarkerTreeKind::String(kind) => {
|
|
for (tree, range) in simplify::collect_edges(kind.children()) {
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{key}{range} -> ", key = kind.key())?;
|
|
tree.fmt_graph(f, level + 1)?;
|
|
}
|
|
}
|
|
MarkerTreeKind::In(kind) => {
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "{} in {} -> ", kind.key(), kind.value())?;
|
|
kind.edge(true).fmt_graph(f, level + 1)?;
|
|
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "{} not in {} -> ", kind.key(), kind.value())?;
|
|
kind.edge(false).fmt_graph(f, level + 1)?;
|
|
}
|
|
MarkerTreeKind::Contains(kind) => {
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "{} in {} -> ", kind.value(), kind.key())?;
|
|
kind.edge(true).fmt_graph(f, level + 1)?;
|
|
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "{} not in {} -> ", kind.value(), kind.key())?;
|
|
kind.edge(false).fmt_graph(f, level + 1)?;
|
|
}
|
|
MarkerTreeKind::Extra(kind) => {
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "extra == {} -> ", kind.name())?;
|
|
kind.edge(true).fmt_graph(f, level + 1)?;
|
|
|
|
writeln!(f)?;
|
|
for _ in 0..level {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "extra != {} -> ", kind.name())?;
|
|
kind.edge(false).fmt_graph(f, level + 1)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for MarkerTree {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for MarkerTree {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.kind().cmp(&other.kind())
|
|
}
|
|
}
|
|
|
|
/// Formats a [`MarkerTree`] as a graph.
|
|
///
|
|
/// This type is created by the [`MarkerTree::debug_graph`] routine.
|
|
#[derive(Clone)]
|
|
pub struct MarkerTreeDebugGraph<'a> {
|
|
marker: &'a MarkerTree,
|
|
}
|
|
|
|
impl<'a> fmt::Debug for MarkerTreeDebugGraph<'a> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
self.marker.fmt_graph(f, 0)
|
|
}
|
|
}
|
|
|
|
/// Formats a [`MarkerTree`] using its raw internals.
|
|
///
|
|
/// This is very verbose and likely only useful if you're working
|
|
/// on the internals of this crate.
|
|
///
|
|
/// This type is created by the [`MarkerTree::debug_raw`] routine.
|
|
#[derive(Clone)]
|
|
pub struct MarkerTreeDebugRaw<'a> {
|
|
marker: &'a MarkerTree,
|
|
}
|
|
|
|
impl<'a> fmt::Debug for MarkerTreeDebugRaw<'a> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let node = INTERNER.shared.node(self.marker.0);
|
|
f.debug_tuple("MarkerTreeDebugRaw").field(node).finish()
|
|
}
|
|
}
|
|
|
|
/// The underlying kind of an arbitrary node in a [`MarkerTree`].
|
|
///
|
|
/// A marker tree is represented as an algebraic decision tree with two terminal nodes
|
|
/// `True` or `False`. The edges of a given node correspond to a particular assignment of
|
|
/// a value to that variable.
|
|
#[derive(PartialEq, Eq, Clone, Debug, PartialOrd, Ord)]
|
|
pub enum MarkerTreeKind<'a> {
|
|
/// An empty marker that always evaluates to `true`.
|
|
True,
|
|
/// An unsatisfiable marker that always evaluates to `false`.
|
|
False,
|
|
/// A version expression.
|
|
Version(VersionMarkerTree<'a>),
|
|
/// A string expression.
|
|
String(StringMarkerTree<'a>),
|
|
/// A string expression with the `in` operator.
|
|
In(InMarkerTree<'a>),
|
|
/// A string expression with the `contains` operator.
|
|
Contains(ContainsMarkerTree<'a>),
|
|
/// A string expression.
|
|
Extra(ExtraMarkerTree<'a>),
|
|
}
|
|
|
|
/// A version marker node, such as `python_version < '3.7'`.
|
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
|
pub struct VersionMarkerTree<'a> {
|
|
id: NodeId,
|
|
key: CanonicalMarkerValueVersion,
|
|
map: &'a [(Ranges<Version>, NodeId)],
|
|
}
|
|
|
|
impl VersionMarkerTree<'_> {
|
|
/// The key for this node.
|
|
pub fn key(&self) -> CanonicalMarkerValueVersion {
|
|
self.key
|
|
}
|
|
|
|
/// The edges of this node, corresponding to possible output ranges of the given variable.
|
|
pub fn edges(&self) -> impl ExactSizeIterator<Item = (&Ranges<Version>, MarkerTree)> + '_ {
|
|
self.map
|
|
.iter()
|
|
.map(|(range, node)| (range, MarkerTree(node.negate(self.id))))
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for VersionMarkerTree<'_> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for VersionMarkerTree<'_> {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.key()
|
|
.cmp(&other.key())
|
|
.then_with(|| self.edges().cmp(other.edges()))
|
|
}
|
|
}
|
|
|
|
/// A string marker node, such as `os_name == 'Linux'`.
|
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
|
pub struct StringMarkerTree<'a> {
|
|
id: NodeId,
|
|
key: CanonicalMarkerValueString,
|
|
map: &'a [(Ranges<String>, NodeId)],
|
|
}
|
|
|
|
impl StringMarkerTree<'_> {
|
|
/// The key for this node.
|
|
pub fn key(&self) -> CanonicalMarkerValueString {
|
|
self.key
|
|
}
|
|
|
|
/// The edges of this node, corresponding to possible output ranges of the given variable.
|
|
pub fn children(&self) -> impl ExactSizeIterator<Item = (&Ranges<String>, MarkerTree)> {
|
|
self.map
|
|
.iter()
|
|
.map(|(range, node)| (range, MarkerTree(node.negate(self.id))))
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for StringMarkerTree<'_> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for StringMarkerTree<'_> {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.key()
|
|
.cmp(&other.key())
|
|
.then_with(|| self.children().cmp(other.children()))
|
|
}
|
|
}
|
|
|
|
/// A string marker node with the `in` operator, such as `os_name in 'WindowsLinux'`.
|
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
|
pub struct InMarkerTree<'a> {
|
|
key: CanonicalMarkerValueString,
|
|
value: &'a str,
|
|
high: NodeId,
|
|
low: NodeId,
|
|
}
|
|
|
|
impl InMarkerTree<'_> {
|
|
/// The key (LHS) for this expression.
|
|
pub fn key(&self) -> CanonicalMarkerValueString {
|
|
self.key
|
|
}
|
|
|
|
/// The value (RHS) for this expression.
|
|
pub fn value(&self) -> &str {
|
|
self.value
|
|
}
|
|
|
|
/// The edges of this node, corresponding to the boolean evaluation of the expression.
|
|
pub fn children(&self) -> impl Iterator<Item = (bool, MarkerTree)> {
|
|
[(true, MarkerTree(self.high)), (false, MarkerTree(self.low))].into_iter()
|
|
}
|
|
|
|
/// Returns the subtree associated with the given edge value.
|
|
pub fn edge(&self, value: bool) -> MarkerTree {
|
|
if value {
|
|
MarkerTree(self.high)
|
|
} else {
|
|
MarkerTree(self.low)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for InMarkerTree<'_> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for InMarkerTree<'_> {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.key()
|
|
.cmp(&other.key())
|
|
.then_with(|| self.value().cmp(other.value()))
|
|
.then_with(|| self.children().cmp(other.children()))
|
|
}
|
|
}
|
|
|
|
/// A string marker node with inverse of the `in` operator, such as `'nux' in os_name`.
|
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
|
pub struct ContainsMarkerTree<'a> {
|
|
key: CanonicalMarkerValueString,
|
|
value: &'a str,
|
|
high: NodeId,
|
|
low: NodeId,
|
|
}
|
|
|
|
impl ContainsMarkerTree<'_> {
|
|
/// The key (LHS) for this expression.
|
|
pub fn key(&self) -> CanonicalMarkerValueString {
|
|
self.key
|
|
}
|
|
|
|
/// The value (RHS) for this expression.
|
|
pub fn value(&self) -> &str {
|
|
self.value
|
|
}
|
|
|
|
/// The edges of this node, corresponding to the boolean evaluation of the expression.
|
|
pub fn children(&self) -> impl Iterator<Item = (bool, MarkerTree)> {
|
|
[(true, MarkerTree(self.high)), (false, MarkerTree(self.low))].into_iter()
|
|
}
|
|
|
|
/// Returns the subtree associated with the given edge value.
|
|
pub fn edge(&self, value: bool) -> MarkerTree {
|
|
if value {
|
|
MarkerTree(self.high)
|
|
} else {
|
|
MarkerTree(self.low)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for ContainsMarkerTree<'_> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for ContainsMarkerTree<'_> {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.key()
|
|
.cmp(&other.key())
|
|
.then_with(|| self.value().cmp(other.value()))
|
|
.then_with(|| self.children().cmp(other.children()))
|
|
}
|
|
}
|
|
|
|
/// A node representing the existence or absence of a given extra, such as `extra == 'bar'`.
|
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
|
pub struct ExtraMarkerTree<'a> {
|
|
name: &'a CanonicalMarkerValueExtra,
|
|
high: NodeId,
|
|
low: NodeId,
|
|
}
|
|
|
|
impl ExtraMarkerTree<'_> {
|
|
/// Returns the name of the extra in this expression.
|
|
pub fn name(&self) -> &CanonicalMarkerValueExtra {
|
|
self.name
|
|
}
|
|
|
|
/// The edges of this node, corresponding to the boolean evaluation of the expression.
|
|
pub fn children(&self) -> impl Iterator<Item = (bool, MarkerTree)> {
|
|
[(true, MarkerTree(self.high)), (false, MarkerTree(self.low))].into_iter()
|
|
}
|
|
|
|
/// Returns the subtree associated with the given edge value.
|
|
pub fn edge(&self, value: bool) -> MarkerTree {
|
|
if value {
|
|
MarkerTree(self.high)
|
|
} else {
|
|
MarkerTree(self.low)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for ExtraMarkerTree<'_> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for ExtraMarkerTree<'_> {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.name()
|
|
.cmp(other.name())
|
|
.then_with(|| self.children().cmp(other.children()))
|
|
}
|
|
}
|
|
|
|
/// A marker tree that contains at least one expression.
|
|
///
|
|
/// See [`MarkerTree::contents`] for details.
|
|
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd, Ord, Debug)]
|
|
pub struct MarkerTreeContents(MarkerTree);
|
|
|
|
impl From<MarkerTreeContents> for MarkerTree {
|
|
fn from(contents: MarkerTreeContents) -> Self {
|
|
contents.0
|
|
}
|
|
}
|
|
|
|
impl From<Option<MarkerTreeContents>> for MarkerTree {
|
|
fn from(marker: Option<MarkerTreeContents>) -> Self {
|
|
marker.map(|contents| contents.0).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
impl AsRef<MarkerTree> for MarkerTreeContents {
|
|
fn as_ref(&self) -> &MarkerTree {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl Serialize for MarkerTreeContents {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_str(&self.to_string())
|
|
}
|
|
}
|
|
|
|
impl Display for MarkerTreeContents {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
// Normalize all `false` expressions to the same trivially false expression.
|
|
if self.0.is_false() {
|
|
return write!(f, "python_version < '0'");
|
|
}
|
|
|
|
// Write the output in DNF form.
|
|
let dnf = self.0.to_dnf();
|
|
let format_conjunction = |conjunction: &Vec<MarkerExpression>| {
|
|
conjunction
|
|
.iter()
|
|
.map(MarkerExpression::to_string)
|
|
.collect::<Vec<String>>()
|
|
.join(" and ")
|
|
};
|
|
|
|
let expr = match &dnf[..] {
|
|
[conjunction] => format_conjunction(conjunction),
|
|
_ => dnf
|
|
.iter()
|
|
.map(|conjunction| {
|
|
if conjunction.len() == 1 {
|
|
format_conjunction(conjunction)
|
|
} else {
|
|
format!("({})", format_conjunction(conjunction))
|
|
}
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.join(" or "),
|
|
};
|
|
|
|
f.write_str(&expr)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "schemars")]
|
|
impl schemars::JsonSchema for MarkerTree {
|
|
fn schema_name() -> String {
|
|
"MarkerTree".to_string()
|
|
}
|
|
|
|
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
|
schemars::schema::SchemaObject {
|
|
instance_type: Some(schemars::schema::InstanceType::String.into()),
|
|
metadata: Some(Box::new(schemars::schema::Metadata {
|
|
description: Some(
|
|
"A PEP 508-compliant marker expression, e.g., `sys_platform == 'Darwin'`"
|
|
.to_string(),
|
|
),
|
|
..schemars::schema::Metadata::default()
|
|
})),
|
|
..schemars::schema::SchemaObject::default()
|
|
}
|
|
.into()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::ops::Bound;
|
|
use std::str::FromStr;
|
|
|
|
use insta::assert_snapshot;
|
|
use uv_normalize::ExtraName;
|
|
use uv_pep440::Version;
|
|
|
|
use crate::marker::{MarkerEnvironment, MarkerEnvironmentBuilder};
|
|
use crate::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
|
|
|
|
fn parse_err(input: &str) -> String {
|
|
MarkerTree::from_str(input).unwrap_err().to_string()
|
|
}
|
|
|
|
fn m(s: &str) -> MarkerTree {
|
|
s.parse().unwrap()
|
|
}
|
|
|
|
fn env37() -> MarkerEnvironment {
|
|
MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
|
|
implementation_name: "",
|
|
implementation_version: "3.7",
|
|
os_name: "linux",
|
|
platform_machine: "",
|
|
platform_python_implementation: "",
|
|
platform_release: "",
|
|
platform_system: "",
|
|
platform_version: "",
|
|
python_full_version: "3.7",
|
|
python_version: "3.7",
|
|
sys_platform: "linux",
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
/// Copied from <https://github.com/pypa/packaging/blob/85ff971a250dc01db188ef9775499c15553a8c95/tests/test_markers.py#L175-L221>
|
|
#[test]
|
|
fn test_marker_equivalence() {
|
|
let values = [
|
|
(r"python_version == '2.7'", r#"python_version == "2.7""#),
|
|
(r#"python_version == "2.7""#, r#"python_version == "2.7""#),
|
|
(
|
|
r#"python_version == "2.7" and os_name == "posix""#,
|
|
r#"python_version == "2.7" and os_name == "posix""#,
|
|
),
|
|
(
|
|
r#"python_version == "2.7" or os_name == "posix""#,
|
|
r#"python_version == "2.7" or os_name == "posix""#,
|
|
),
|
|
(
|
|
r#"python_version == "2.7" and os_name == "posix" or sys_platform == "win32""#,
|
|
r#"python_version == "2.7" and os_name == "posix" or sys_platform == "win32""#,
|
|
),
|
|
(r#"(python_version == "2.7")"#, r#"python_version == "2.7""#),
|
|
(
|
|
r#"(python_version == "2.7" and sys_platform == "win32")"#,
|
|
r#"python_version == "2.7" and sys_platform == "win32""#,
|
|
),
|
|
(
|
|
r#"python_version == "2.7" and (sys_platform == "win32" or sys_platform == "linux")"#,
|
|
r#"python_version == "2.7" and (sys_platform == "win32" or sys_platform == "linux")"#,
|
|
),
|
|
];
|
|
|
|
for (a, b) in values {
|
|
assert_eq!(m(a), m(b), "{a} {b}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn simplify_python_versions() {
|
|
assert_eq!(
|
|
m("(extra == 'foo' and sys_platform == 'win32') or extra == 'foo'")
|
|
.simplify_extras(&["foo".parse().unwrap()]),
|
|
MarkerTree::TRUE
|
|
);
|
|
|
|
assert_eq!(
|
|
m("(python_version <= '3.11' and sys_platform == 'win32') or python_version > '3.11'")
|
|
.simplify_python_versions(
|
|
Bound::Excluded(Version::new([3, 12])).as_ref(),
|
|
Bound::Unbounded.as_ref(),
|
|
),
|
|
MarkerTree::TRUE
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_version < '3.10'")
|
|
.simplify_python_versions(
|
|
Bound::Excluded(Version::new([3, 7])).as_ref(),
|
|
Bound::Unbounded.as_ref(),
|
|
)
|
|
.try_to_string()
|
|
.unwrap(),
|
|
"python_full_version < '3.10'"
|
|
);
|
|
|
|
// Note that `3.12.1` will still match.
|
|
assert_eq!(
|
|
m("python_version <= '3.12'")
|
|
.simplify_python_versions(
|
|
Bound::Excluded(Version::new([3, 12])).as_ref(),
|
|
Bound::Unbounded.as_ref(),
|
|
)
|
|
.try_to_string()
|
|
.unwrap(),
|
|
"python_full_version < '3.13'"
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_full_version <= '3.12'").simplify_python_versions(
|
|
Bound::Excluded(Version::new([3, 12])).as_ref(),
|
|
Bound::Unbounded.as_ref(),
|
|
),
|
|
MarkerTree::FALSE
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_full_version <= '3.12.1'")
|
|
.simplify_python_versions(
|
|
Bound::Excluded(Version::new([3, 12])).as_ref(),
|
|
Bound::Unbounded.as_ref(),
|
|
)
|
|
.try_to_string()
|
|
.unwrap(),
|
|
"python_full_version <= '3.12.1'"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn release_only() {
|
|
assert!(m("python_full_version > '3.10' or python_full_version <= '3.10'").is_true());
|
|
assert!(
|
|
m("python_full_version > '3.10' or python_full_version <= '3.10'")
|
|
.negate()
|
|
.is_false()
|
|
);
|
|
assert!(m("python_full_version > '3.10' and python_full_version <= '3.10'").is_false());
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_evaluation() {
|
|
let env27 = MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
|
|
implementation_name: "",
|
|
implementation_version: "2.7",
|
|
os_name: "linux",
|
|
platform_machine: "",
|
|
platform_python_implementation: "",
|
|
platform_release: "",
|
|
platform_system: "",
|
|
platform_version: "",
|
|
python_full_version: "2.7",
|
|
python_version: "2.7",
|
|
sys_platform: "linux",
|
|
})
|
|
.unwrap();
|
|
let env37 = env37();
|
|
let marker1 = MarkerTree::from_str("python_version == '2.7'").unwrap();
|
|
let marker2 = MarkerTree::from_str(
|
|
"os_name == \"linux\" or python_version == \"3.7\" and sys_platform == \"win32\"",
|
|
)
|
|
.unwrap();
|
|
let marker3 = MarkerTree::from_str(
|
|
"python_version == \"2.7\" and (sys_platform == \"win32\" or sys_platform == \"linux\")",
|
|
).unwrap();
|
|
assert!(marker1.evaluate(&env27, &[]));
|
|
assert!(!marker1.evaluate(&env37, &[]));
|
|
assert!(marker2.evaluate(&env27, &[]));
|
|
assert!(marker2.evaluate(&env37, &[]));
|
|
assert!(marker3.evaluate(&env27, &[]));
|
|
assert!(!marker3.evaluate(&env37, &[]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_in_evaluation() {
|
|
let env27 = MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
|
|
implementation_name: "",
|
|
implementation_version: "2.7",
|
|
os_name: "linux",
|
|
platform_machine: "",
|
|
platform_python_implementation: "",
|
|
platform_release: "",
|
|
platform_system: "",
|
|
platform_version: "",
|
|
python_full_version: "2.7",
|
|
python_version: "2.7",
|
|
sys_platform: "linux",
|
|
})
|
|
.unwrap();
|
|
let env37 = env37();
|
|
|
|
let marker = MarkerTree::from_str("python_version in \"2.7 3.2 3.3\"").unwrap();
|
|
assert!(marker.evaluate(&env27, &[]));
|
|
assert!(!marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("python_version in \"2.7 3.7\"").unwrap();
|
|
assert!(marker.evaluate(&env27, &[]));
|
|
assert!(marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("python_version in \"2.4 3.8 4.0\"").unwrap();
|
|
assert!(!marker.evaluate(&env27, &[]));
|
|
assert!(!marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("python_version not in \"2.7 3.2 3.3\"").unwrap();
|
|
assert!(!marker.evaluate(&env27, &[]));
|
|
assert!(marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("python_version not in \"2.7 3.7\"").unwrap();
|
|
assert!(!marker.evaluate(&env27, &[]));
|
|
assert!(!marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("python_version not in \"2.4 3.8 4.0\"").unwrap();
|
|
assert!(marker.evaluate(&env27, &[]));
|
|
assert!(marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("python_full_version in \"2.7\"").unwrap();
|
|
assert!(marker.evaluate(&env27, &[]));
|
|
assert!(!marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("implementation_version in \"2.7 3.2 3.3\"").unwrap();
|
|
assert!(marker.evaluate(&env27, &[]));
|
|
assert!(!marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("implementation_version in \"2.7 3.7\"").unwrap();
|
|
assert!(marker.evaluate(&env27, &[]));
|
|
assert!(marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("implementation_version not in \"2.7 3.7\"").unwrap();
|
|
assert!(!marker.evaluate(&env27, &[]));
|
|
assert!(!marker.evaluate(&env37, &[]));
|
|
|
|
let marker = MarkerTree::from_str("implementation_version not in \"2.4 3.8 4.0\"").unwrap();
|
|
assert!(marker.evaluate(&env27, &[]));
|
|
assert!(marker.evaluate(&env37, &[]));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "tracing")]
|
|
#[tracing_test::traced_test]
|
|
fn warnings1() {
|
|
let env37 = env37();
|
|
let compare_keys = MarkerTree::from_str("platform_version == sys_platform").unwrap();
|
|
compare_keys.evaluate(&env37, &[]);
|
|
logs_contain(
|
|
"Comparing two markers with each other doesn't make any sense, will evaluate to false",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "tracing")]
|
|
#[tracing_test::traced_test]
|
|
fn warnings2() {
|
|
let env37 = env37();
|
|
let non_pep440 = MarkerTree::from_str("python_version >= '3.9.'").unwrap();
|
|
non_pep440.evaluate(&env37, &[]);
|
|
logs_contain(
|
|
"Expected PEP 440 version to compare with python_version, found `3.9.`, \
|
|
will evaluate to false: after parsing `3.9`, found `.`, which is \
|
|
not part of a valid version",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "tracing")]
|
|
#[tracing_test::traced_test]
|
|
fn warnings3() {
|
|
let env37 = env37();
|
|
let string_string = MarkerTree::from_str("'b' >= 'a'").unwrap();
|
|
string_string.evaluate(&env37, &[]);
|
|
logs_contain(
|
|
"Comparing two quoted strings with each other doesn't make sense: 'b' >= 'a', will evaluate to false"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "tracing")]
|
|
#[tracing_test::traced_test]
|
|
fn warnings4() {
|
|
let env37 = env37();
|
|
let string_string = MarkerTree::from_str(r"os.name == 'posix' and platform.machine == 'x86_64' and platform.python_implementation == 'CPython' and 'Ubuntu' in platform.version and sys.platform == 'linux'").unwrap();
|
|
string_string.evaluate(&env37, &[]);
|
|
logs_assert(|lines: &[&str]| {
|
|
let lines: Vec<_> = lines
|
|
.iter()
|
|
.map(|s| s.split_once(" ").unwrap().1)
|
|
.collect();
|
|
let expected = [
|
|
"WARN warnings4: uv_pep508: os.name is deprecated in favor of os_name",
|
|
"WARN warnings4: uv_pep508: platform.machine is deprecated in favor of platform_machine",
|
|
"WARN warnings4: uv_pep508: platform.python_implementation is deprecated in favor of platform_python_implementation",
|
|
"WARN warnings4: uv_pep508: platform.version is deprecated in favor of platform_version",
|
|
"WARN warnings4: uv_pep508: sys.platform is deprecated in favor of sys_platform",
|
|
"WARN warnings4: uv_pep508: Comparing linux and posix lexicographically"
|
|
];
|
|
if lines == expected {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("{lines:#?}"))
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_not_in() {
|
|
MarkerTree::from_str("'posix' not in os_name").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_version_inverted() {
|
|
let env37 = env37();
|
|
let result = MarkerTree::from_str("python_version > '3.6'")
|
|
.unwrap()
|
|
.evaluate(&env37, &[]);
|
|
assert!(result);
|
|
|
|
let result = MarkerTree::from_str("'3.6' > python_version")
|
|
.unwrap()
|
|
.evaluate(&env37, &[]);
|
|
assert!(!result);
|
|
|
|
// Meaningless expressions are ignored, so this is always true.
|
|
let result = MarkerTree::from_str("'3.*' == python_version")
|
|
.unwrap()
|
|
.evaluate(&env37, &[]);
|
|
assert!(result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_string_inverted() {
|
|
let env37 = env37();
|
|
let result = MarkerTree::from_str("'nux' in sys_platform")
|
|
.unwrap()
|
|
.evaluate(&env37, &[]);
|
|
assert!(result);
|
|
|
|
let result = MarkerTree::from_str("sys_platform in 'nux'")
|
|
.unwrap()
|
|
.evaluate(&env37, &[]);
|
|
assert!(!result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_version_star() {
|
|
let env37 = env37();
|
|
let result = MarkerTree::from_str("python_version == '3.7.*'")
|
|
.unwrap()
|
|
.evaluate(&env37, &[]);
|
|
assert!(result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tilde_equal() {
|
|
let env37 = env37();
|
|
let result = MarkerTree::from_str("python_version ~= '3.7'")
|
|
.unwrap()
|
|
.evaluate(&env37, &[]);
|
|
assert!(result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_closing_parentheses() {
|
|
MarkerTree::from_str(r#"( "linux" in sys_platform) and extra == 'all'"#).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_quotes_dot_star() {
|
|
assert_snapshot!(
|
|
parse_err(r#"python_version == "3.8".* and python_version >= "3.8""#),
|
|
@r#"
|
|
Unexpected character '.', expected 'and', 'or' or end of input
|
|
python_version == "3.8".* and python_version >= "3.8"
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"#
|
|
);
|
|
assert_snapshot!(
|
|
parse_err(r#"python_version == "3.8".*"#),
|
|
@r#"
|
|
Unexpected character '.', expected 'and', 'or' or end of input
|
|
python_version == "3.8".*
|
|
^"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_expression() {
|
|
assert_eq!(
|
|
MarkerExpression::from_str(r#"os_name == "nt""#)
|
|
.unwrap()
|
|
.unwrap(),
|
|
MarkerExpression::String {
|
|
key: MarkerValueString::OsName,
|
|
operator: MarkerOperator::Equal,
|
|
value: "nt".to_string(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_expression_inverted() {
|
|
assert_eq!(
|
|
MarkerTree::from_str(
|
|
r#""nt" in os_name and '3.7' >= python_version and python_full_version >= '3.7'"#
|
|
)
|
|
.unwrap()
|
|
.contents()
|
|
.unwrap()
|
|
.to_string(),
|
|
"python_full_version == '3.7.*' and 'nt' in os_name",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_expression_to_long() {
|
|
let err = MarkerExpression::from_str(r#"os_name == "nt" and python_version >= "3.8""#)
|
|
.unwrap_err()
|
|
.to_string();
|
|
assert_snapshot!(
|
|
err,
|
|
@r#"
|
|
Unexpected character 'a', expected end of input
|
|
os_name == "nt" and python_version >= "3.8"
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_environment_from_json() {
|
|
let _env: MarkerEnvironment = serde_json::from_str(
|
|
r##"{
|
|
"implementation_name": "cpython",
|
|
"implementation_version": "3.7.13",
|
|
"os_name": "posix",
|
|
"platform_machine": "x86_64",
|
|
"platform_python_implementation": "CPython",
|
|
"platform_release": "5.4.188+",
|
|
"platform_system": "Linux",
|
|
"platform_version": "#1 SMP Sun Apr 24 10:03:06 PDT 2022",
|
|
"python_full_version": "3.7.13",
|
|
"python_version": "3.7",
|
|
"sys_platform": "linux"
|
|
}"##,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_simplify_extras() {
|
|
// Given `os_name == "nt" and extra == "dev"`, simplify to `os_name == "nt"`.
|
|
let markers = MarkerTree::from_str(r#"os_name == "nt" and extra == "dev""#).unwrap();
|
|
let simplified = markers.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
|
|
let expected = MarkerTree::from_str(r#"os_name == "nt""#).unwrap();
|
|
assert_eq!(simplified, expected);
|
|
|
|
// Given `os_name == "nt" or extra == "dev"`, remove the marker entirely.
|
|
let markers = MarkerTree::from_str(r#"os_name == "nt" or extra == "dev""#).unwrap();
|
|
let simplified = markers.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
|
|
assert_eq!(simplified, MarkerTree::TRUE);
|
|
|
|
// Given `extra == "dev"`, remove the marker entirely.
|
|
let markers = MarkerTree::from_str(r#"extra == "dev""#).unwrap();
|
|
let simplified = markers.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
|
|
assert_eq!(simplified, MarkerTree::TRUE);
|
|
|
|
// Given `extra == "dev" and extra == "test"`, simplify to `extra == "test"`.
|
|
let markers = MarkerTree::from_str(r#"extra == "dev" and extra == "test""#).unwrap();
|
|
let simplified = markers.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
|
|
let expected = MarkerTree::from_str(r#"extra == "test""#).unwrap();
|
|
assert_eq!(simplified, expected);
|
|
|
|
// Given `os_name == "nt" and extra == "test"`, don't simplify.
|
|
let markers = MarkerTree::from_str(r#"os_name == "nt" and extra == "test""#).unwrap();
|
|
let simplified = markers
|
|
.clone()
|
|
.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
|
|
assert_eq!(simplified, markers);
|
|
|
|
// Given `os_name == "nt" and (python_version == "3.7" or extra == "dev")`, simplify to
|
|
// `os_name == "nt".
|
|
let markers = MarkerTree::from_str(
|
|
r#"os_name == "nt" and (python_version == "3.7" or extra == "dev")"#,
|
|
)
|
|
.unwrap();
|
|
let simplified = markers.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
|
|
let expected = MarkerTree::from_str(r#"os_name == "nt""#).unwrap();
|
|
assert_eq!(simplified, expected);
|
|
|
|
// Given `os_name == "nt" or (python_version == "3.7" and extra == "dev")`, simplify to
|
|
// `os_name == "nt" or python_version == "3.7"`.
|
|
let markers = MarkerTree::from_str(
|
|
r#"os_name == "nt" or (python_version == "3.7" and extra == "dev")"#,
|
|
)
|
|
.unwrap();
|
|
let simplified = markers.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
|
|
let expected =
|
|
MarkerTree::from_str(r#"os_name == "nt" or python_version == "3.7""#).unwrap();
|
|
assert_eq!(simplified, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_simplification() {
|
|
assert_false("python_version == '3.9.1'");
|
|
assert_false("python_version == '3.9.0.*'");
|
|
assert_true("python_version != '3.9.1'");
|
|
|
|
// Technically these is are valid substring comparison, but we do not allow them.
|
|
// e.g., using a version with patch components with `python_version` is considered
|
|
// impossible to satisfy since the value it is truncated at the minor version
|
|
assert_false("python_version in '3.9.0'");
|
|
// e.g., using a version that is not PEP 440 compliant is considered arbitrary
|
|
assert_true("python_version in 'foo'");
|
|
// e.g., including `*` versions, which would require tracking a version specifier
|
|
assert_true("python_version in '3.9.*'");
|
|
// e.g., when non-whitespace separators are present
|
|
assert_true("python_version in '3.9, 3.10'");
|
|
assert_true("python_version in '3.9,3.10'");
|
|
assert_true("python_version in '3.9 or 3.10'");
|
|
|
|
// e.g, when one of the values cannot be true
|
|
// TODO(zanieb): This seems like a quirk of the `python_full_version` normalization, this
|
|
// should just act as though the patch version isn't present
|
|
assert_false("python_version in '3.9 3.10.0 3.11'");
|
|
|
|
assert_simplifies("python_version == '3.9'", "python_full_version == '3.9.*'");
|
|
assert_simplifies(
|
|
"python_version == '3.9.0'",
|
|
"python_full_version == '3.9.*'",
|
|
);
|
|
|
|
// `<version> in`
|
|
// e.g., when the range is not contiguous
|
|
assert_simplifies(
|
|
"python_version in '3.9 3.11'",
|
|
"python_full_version == '3.9.*' or python_full_version == '3.11.*'",
|
|
);
|
|
// e.g., when the range is contiguous
|
|
assert_simplifies(
|
|
"python_version in '3.9 3.10 3.11'",
|
|
"python_full_version >= '3.9' and python_full_version < '3.12'",
|
|
);
|
|
// e.g., with `implementation_version` instead of `python_version`
|
|
assert_simplifies(
|
|
"implementation_version in '3.9 3.11'",
|
|
"implementation_version == '3.9' or implementation_version == '3.11'",
|
|
);
|
|
|
|
// '<version> not in'
|
|
// e.g., when the range is not contiguous
|
|
assert_simplifies(
|
|
"python_version not in '3.9 3.11'",
|
|
"python_full_version < '3.9' or python_full_version == '3.10.*' or python_full_version >= '3.12'",
|
|
);
|
|
// e.g, when the range is contiguous
|
|
assert_simplifies(
|
|
"python_version not in '3.9 3.10 3.11'",
|
|
"python_full_version < '3.9' or python_full_version >= '3.12'",
|
|
);
|
|
// e.g., with `implementation_version` instead of `python_version`
|
|
assert_simplifies(
|
|
"implementation_version not in '3.9 3.11'",
|
|
"implementation_version != '3.9' and implementation_version != '3.11'",
|
|
);
|
|
|
|
assert_simplifies("python_version != '3.9'", "python_full_version != '3.9.*'");
|
|
|
|
assert_simplifies("python_version >= '3.9.0'", "python_full_version >= '3.9'");
|
|
assert_simplifies("python_version <= '3.9.0'", "python_full_version < '3.10'");
|
|
|
|
assert_simplifies(
|
|
"python_version == '3.*'",
|
|
"python_full_version >= '3' and python_full_version < '4'",
|
|
);
|
|
assert_simplifies(
|
|
"python_version == '3.0.*'",
|
|
"python_full_version == '3.0.*'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version < '3.17' or python_version < '3.18'",
|
|
"python_full_version < '3.18'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version > '3.17' or python_version > '3.18' or python_version > '3.12'",
|
|
"python_full_version >= '3.13'",
|
|
);
|
|
|
|
// a quirk of how pubgrub works, but this is considered part of normalization
|
|
assert_simplifies(
|
|
"python_version > '3.17.post4' or python_version > '3.18.post4'",
|
|
"python_full_version >= '3.18'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version < '3.17' and python_version < '3.18'",
|
|
"python_full_version < '3.17'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version <= '3.18' and python_version == '3.18'",
|
|
"python_full_version == '3.18.*'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version <= '3.18' or python_version == '3.18'",
|
|
"python_full_version < '3.19'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version <= '3.15' or (python_version <= '3.17' and python_version < '3.16')",
|
|
"python_full_version < '3.16'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(python_version > '3.17' or python_version > '3.16') and python_version > '3.15'",
|
|
"python_full_version >= '3.17'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(python_version > '3.17' or python_version > '3.16') and python_version > '3.15' and implementation_version == '1'",
|
|
"implementation_version == '1' and python_full_version >= '3.17'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"('3.17' < python_version or '3.16' < python_version) and '3.15' < python_version and implementation_version == '1'",
|
|
"implementation_version == '1' and python_full_version >= '3.17'",
|
|
);
|
|
|
|
assert_simplifies("extra == 'a' or extra == 'a'", "extra == 'a'");
|
|
assert_simplifies(
|
|
"extra == 'a' and extra == 'a' or extra == 'b'",
|
|
"extra == 'a' or extra == 'b'",
|
|
);
|
|
|
|
assert!(m("python_version < '3.17' and '3.18' == python_version").is_false());
|
|
|
|
// flatten nested expressions
|
|
assert_simplifies(
|
|
"((extra == 'a' and extra == 'b') and extra == 'c') and extra == 'b'",
|
|
"extra == 'a' and extra == 'b' and extra == 'c'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"((extra == 'a' or extra == 'b') or extra == 'c') or extra == 'b'",
|
|
"extra == 'a' or extra == 'b' or extra == 'c'",
|
|
);
|
|
|
|
// complex expressions
|
|
assert_simplifies(
|
|
"extra == 'a' or (extra == 'a' and extra == 'b')",
|
|
"extra == 'a'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"extra == 'a' and (extra == 'a' or extra == 'b')",
|
|
"extra == 'a'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(extra == 'a' and (extra == 'a' or extra == 'b')) or extra == 'd'",
|
|
"extra == 'a' or extra == 'd'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"((extra == 'a' and extra == 'b') or extra == 'c') or extra == 'b'",
|
|
"extra == 'b' or extra == 'c'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"((extra == 'a' or extra == 'b') and extra == 'c') and extra == 'b'",
|
|
"extra == 'b' and extra == 'c'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"((extra == 'a' or extra == 'b') and extra == 'c') or extra == 'b'",
|
|
"(extra == 'a' and extra == 'c') or extra == 'b'",
|
|
);
|
|
|
|
// post-normalization filtering
|
|
assert_simplifies(
|
|
"(python_version < '3.1' or python_version < '3.2') and (python_version < '3.2' or python_version == '3.3')",
|
|
"python_full_version < '3.2'",
|
|
);
|
|
|
|
// normalize out redundant ranges
|
|
assert_true("python_version < '3.12.0rc1' or python_version >= '3.12.0rc1'");
|
|
|
|
assert_true(
|
|
"extra == 'a' or (python_version < '3.12.0rc1' or python_version >= '3.12.0rc1')",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"extra == 'a' and (python_version < '3.12.0rc1' or python_version >= '3.12.0rc1')",
|
|
"extra == 'a'",
|
|
);
|
|
|
|
// normalize `!=` operators
|
|
assert_true("python_version != '3.10' or python_version < '3.12'");
|
|
|
|
assert_simplifies(
|
|
"python_version != '3.10' or python_version > '3.12'",
|
|
"python_full_version != '3.10.*'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version != '3.8' and python_version < '3.10'",
|
|
"python_full_version < '3.8' or python_full_version == '3.9.*'",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"python_version != '3.8' and python_version != '3.9'",
|
|
"python_full_version < '3.8' or python_full_version >= '3.10'",
|
|
);
|
|
|
|
// normalize out redundant expressions
|
|
assert_true("sys_platform == 'win32' or sys_platform != 'win32'");
|
|
|
|
assert_true("'win32' == sys_platform or sys_platform != 'win32'");
|
|
|
|
assert_true(
|
|
"sys_platform == 'win32' or sys_platform == 'win32' or sys_platform != 'win32'",
|
|
);
|
|
|
|
assert!(m("sys_platform == 'win32' and sys_platform != 'win32'").is_false());
|
|
}
|
|
|
|
/// This tests the difference between simplifying extras and simplifying
|
|
/// other stringly-valued marker attributes.
|
|
///
|
|
/// In particular, this demonstrates that `extra != "foo"` would actually
|
|
/// be more clearly written as `"foo" not in extras`. And similarly, `extra
|
|
/// == "foo"` would be written as `"foo" in extras`. This is different from
|
|
/// other attributes, like `sys_platform`, where the test is just string
|
|
/// equality. That is, `extra` is a set where as `sys_platform` is just a
|
|
/// single value.
|
|
#[test]
|
|
fn test_simplification_extra_versus_other() {
|
|
// Here, the `extra != 'foo'` cannot be simplified out, because
|
|
// `extra == 'foo'` can be true even when `extra == 'bar`' is true.
|
|
assert_simplifies(
|
|
r#"extra != "foo" and (extra == "bar" or extra == "baz")"#,
|
|
"(extra == 'bar' and extra != 'foo') or (extra == 'baz' and extra != 'foo')",
|
|
);
|
|
// But here, the `sys_platform != 'foo'` can be simplified out, because
|
|
// it is strictly disjoint with
|
|
// `sys_platform == "bar" or sys_platform == "baz"`.
|
|
assert_simplifies(
|
|
r#"sys_platform != "foo" and (sys_platform == "bar" or sys_platform == "baz")"#,
|
|
"sys_platform == 'bar' or sys_platform == 'baz'",
|
|
);
|
|
|
|
// Another case I experimented with and wanted to verify.
|
|
assert_simplifies(
|
|
r#"(extra != "bar" and (extra == "foo" or extra == "baz"))
|
|
or ((extra != "foo" and extra != "bar") and extra == "baz")"#,
|
|
"(extra != 'bar' and extra == 'baz') or (extra != 'bar' and extra == 'foo')",
|
|
);
|
|
}
|
|
|
|
/// This tests marker implication.
|
|
///
|
|
/// Specifically, these test cases come from a [bug] where `foo` and `bar`
|
|
/// are conflicting extras, but results in an ambiguous lock file. This
|
|
/// test takes `not(extra == 'foo' and extra == 'bar')` as world knowledge.
|
|
/// That is, since they are declared as conflicting, they cannot both be
|
|
/// true. This is in turn used to determine whether particular markers are
|
|
/// implied by this world knowledge and thus can be removed.
|
|
///
|
|
/// [bug]: <https://github.com/astral-sh/uv/issues/9289>
|
|
#[test]
|
|
fn test_extra_implication() {
|
|
assert!(implies(
|
|
"extra != 'foo' or extra != 'bar'",
|
|
"extra != 'foo' or extra != 'bar'",
|
|
));
|
|
assert!(!implies(
|
|
"extra != 'foo' or extra != 'bar'",
|
|
"extra != 'foo'",
|
|
));
|
|
assert!(!implies(
|
|
"extra != 'foo' or extra != 'bar'",
|
|
"extra != 'bar' and extra == 'foo'",
|
|
));
|
|
|
|
// This simulates the case when multiple groups of conflicts
|
|
// are combined into one "world knowledge" marker.
|
|
assert!(implies(
|
|
"(extra != 'foo' or extra != 'bar') and (extra != 'baz' or extra != 'quux')",
|
|
"extra != 'foo' or extra != 'bar'",
|
|
));
|
|
assert!(!implies(
|
|
"(extra != 'foo' or extra != 'bar') and (extra != 'baz' or extra != 'quux')",
|
|
"extra != 'foo'",
|
|
));
|
|
assert!(!implies(
|
|
"(extra != 'foo' or extra != 'bar') and (extra != 'baz' or extra != 'quux')",
|
|
"extra != 'bar' and extra == 'foo'",
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_marker_negation() {
|
|
assert_eq!(
|
|
m("python_version > '3.6'").negate(),
|
|
m("python_version <= '3.6'")
|
|
);
|
|
|
|
assert_eq!(
|
|
m("'3.6' < python_version").negate(),
|
|
m("python_version <= '3.6'")
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_version != '3.6' and os_name == 'Linux'").negate(),
|
|
m("python_version == '3.6' or os_name != 'Linux'")
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_version == '3.6' and os_name != 'Linux'").negate(),
|
|
m("python_version != '3.6' or os_name == 'Linux'")
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_version != '3.6.*' and os_name == 'Linux'").negate(),
|
|
m("python_version == '3.6.*' or os_name != 'Linux'")
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_version == '3.6.*'").negate(),
|
|
m("python_version != '3.6.*'")
|
|
);
|
|
assert_eq!(
|
|
m("python_version != '3.6.*'").negate(),
|
|
m("python_version == '3.6.*'")
|
|
);
|
|
|
|
assert_eq!(
|
|
m("python_version ~= '3.6'").negate(),
|
|
m("python_version < '3.6' or python_version != '3.*'")
|
|
);
|
|
assert_eq!(
|
|
m("'3.6' ~= python_version").negate(),
|
|
m("python_version < '3.6' or python_version != '3.*'")
|
|
);
|
|
assert_eq!(
|
|
m("python_version ~= '3.6.2'").negate(),
|
|
m("python_version < '3.6.2' or python_version != '3.6.*'")
|
|
);
|
|
|
|
assert_eq!(
|
|
m("sys_platform == 'linux'").negate(),
|
|
m("sys_platform != 'linux'")
|
|
);
|
|
assert_eq!(
|
|
m("'linux' == sys_platform").negate(),
|
|
m("sys_platform != 'linux'")
|
|
);
|
|
|
|
// ~= is nonsense on string markers, so the markers is ignored and always
|
|
// evaluates to true. Thus the negation always returns false.
|
|
assert_eq!(m("sys_platform ~= 'linux'").negate(), MarkerTree::FALSE);
|
|
|
|
// As above, arbitrary exprs remain arbitrary.
|
|
assert_eq!(m("'foo' == 'bar'").negate(), MarkerTree::FALSE);
|
|
|
|
// Conjunctions
|
|
assert_eq!(
|
|
m("os_name == 'bar' and os_name == 'foo'").negate(),
|
|
m("os_name != 'bar' or os_name != 'foo'")
|
|
);
|
|
// Disjunctions
|
|
assert_eq!(
|
|
m("os_name == 'bar' or os_name == 'foo'").negate(),
|
|
m("os_name != 'bar' and os_name != 'foo'")
|
|
);
|
|
|
|
// Always true negates to always false!
|
|
assert_eq!(
|
|
m("python_version >= '3.6' or python_version < '3.6'").negate(),
|
|
m("python_version < '3.6' and python_version >= '3.6'")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_complex_marker_simplification() {
|
|
// This expression should simplify to:
|
|
// `(implementation_name == 'pypy' and sys_platform != 'win32')
|
|
// or (sys_platform == 'win32' or os_name != 'nt')
|
|
// or (implementation != 'pypy' or os_name == 'nt')`
|
|
//
|
|
// However, simplifying this expression is NP-complete and requires an exponential
|
|
// algorithm such as Quine-McCluskey, which is not currently implemented.
|
|
assert_simplifies(
|
|
"(implementation_name == 'pypy' and sys_platform != 'win32')
|
|
or (implementation_name != 'pypy' and sys_platform == 'win32')
|
|
or (sys_platform == 'win32' and os_name != 'nt')
|
|
or (sys_platform != 'win32' and os_name == 'nt')",
|
|
"(os_name != 'nt' and sys_platform == 'win32') \
|
|
or (implementation_name != 'pypy' and os_name == 'nt') \
|
|
or (implementation_name == 'pypy' and os_name != 'nt') \
|
|
or (os_name == 'nt' and sys_platform != 'win32')",
|
|
);
|
|
|
|
// This is another case we cannot simplify fully, depending on the variable order.
|
|
// The expression is equivalent to `sys_platform == 'x' or (os_name == 'Linux' and platform_system == 'win32')`.
|
|
assert_simplifies(
|
|
"(os_name == 'Linux' and platform_system == 'win32')
|
|
or (os_name == 'Linux' and platform_system == 'win32' and sys_platform == 'a')
|
|
or (os_name == 'Linux' and platform_system == 'win32' and sys_platform == 'x')
|
|
or (os_name != 'Linux' and platform_system == 'win32' and sys_platform == 'x')
|
|
or (os_name == 'Linux' and platform_system != 'win32' and sys_platform == 'x')
|
|
or (os_name != 'Linux' and platform_system != 'win32' and sys_platform == 'x')",
|
|
"(os_name != 'Linux' and sys_platform == 'x') or (platform_system != 'win32' and sys_platform == 'x') or (os_name == 'Linux' and platform_system == 'win32')",
|
|
);
|
|
|
|
assert_simplifies("python_version > '3.7'", "python_full_version >= '3.8'");
|
|
|
|
assert_simplifies(
|
|
"(python_version <= '3.7' and os_name == 'Linux') or python_version > '3.7'",
|
|
"os_name == 'Linux' or python_full_version >= '3.8'",
|
|
);
|
|
|
|
// Again, the extra `<3.7` and `>=3.9` expressions cannot be seen as redundant due to them being interdependent.
|
|
// TODO(ibraheem): We might be able to simplify these by checking for the negation of the combined ranges before we split them.
|
|
assert_simplifies(
|
|
"(os_name == 'Linux' and sys_platform == 'win32') \
|
|
or (os_name != 'Linux' and sys_platform == 'win32' and python_version == '3.7') \
|
|
or (os_name != 'Linux' and sys_platform == 'win32' and python_version == '3.8')",
|
|
"(python_full_version < '3.7' and os_name == 'Linux' and sys_platform == 'win32') \
|
|
or (python_full_version >= '3.9' and os_name == 'Linux' and sys_platform == 'win32') \
|
|
or (python_full_version >= '3.7' and python_full_version < '3.9' and sys_platform == 'win32')",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32')",
|
|
"(implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32')",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(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'))",
|
|
"(implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32')",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(sys_platform == 'darwin' or sys_platform == 'win32')
|
|
and ((platform_version != '1' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32'))",
|
|
"(os_name == 'nt' and platform_version != '1' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32')",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(os_name == 'nt' and sys_platform == 'win32') \
|
|
or (os_name != 'nt' and platform_version == '1' and (sys_platform == 'win32' or sys_platform == 'win64'))",
|
|
"(platform_version == '1' and sys_platform == 'win32') \
|
|
or (os_name != 'nt' and platform_version == '1' and sys_platform == 'win64') \
|
|
or (os_name == 'nt' and sys_platform == 'win32')",
|
|
);
|
|
|
|
assert_simplifies(
|
|
"(os_name == 'nt' and sys_platform == 'win32') or (os_name != 'nt' and (sys_platform == 'win32' or sys_platform == 'win64'))",
|
|
"(os_name != 'nt' and sys_platform == 'win64') or sys_platform == 'win32'",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_requires_python() {
|
|
fn simplified(marker: &str) -> MarkerTree {
|
|
let lower = Bound::Included(Version::new([3, 8]));
|
|
let upper = Bound::Unbounded;
|
|
m(marker).simplify_python_versions(lower.as_ref(), upper.as_ref())
|
|
}
|
|
|
|
assert_eq!(simplified("python_version >= '3.8'"), MarkerTree::TRUE);
|
|
assert_eq!(
|
|
simplified("python_version >= '3.8' or sys_platform == 'win32'"),
|
|
MarkerTree::TRUE
|
|
);
|
|
|
|
assert_eq!(
|
|
simplified("python_version >= '3.8' and sys_platform == 'win32'"),
|
|
m("sys_platform == 'win32'"),
|
|
);
|
|
|
|
assert_eq!(
|
|
simplified("python_version == '3.8'")
|
|
.try_to_string()
|
|
.unwrap(),
|
|
"python_full_version < '3.9'"
|
|
);
|
|
|
|
assert_eq!(
|
|
simplified("python_version <= '3.10'")
|
|
.try_to_string()
|
|
.unwrap(),
|
|
"python_full_version < '3.11'"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extra_disjointness() {
|
|
assert!(!is_disjoint("extra == 'a'", "python_version == '1'"));
|
|
|
|
assert!(!is_disjoint("extra == 'a'", "extra == 'a'"));
|
|
assert!(!is_disjoint("extra == 'a'", "extra == 'b'"));
|
|
assert!(!is_disjoint("extra == 'b'", "extra == 'a'"));
|
|
assert!(!is_disjoint("extra == 'b'", "extra != 'a'"));
|
|
assert!(!is_disjoint("extra != 'b'", "extra == 'a'"));
|
|
assert!(is_disjoint("extra != 'b'", "extra == 'b'"));
|
|
assert!(is_disjoint("extra == 'b'", "extra != 'b'"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_arbitrary_disjointness() {
|
|
// `python_version == 'Linux'` is nonsense and ignored, thus the first marker
|
|
// is always `true` and not disjoint.
|
|
assert!(!is_disjoint(
|
|
"python_version == 'Linux'",
|
|
"python_full_version == '3.7.1'"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_disjointness() {
|
|
assert!(!is_disjoint(
|
|
"os_name == 'Linux'",
|
|
"python_full_version == '3.7.1'"
|
|
));
|
|
|
|
test_version_bounds_disjointness("python_full_version");
|
|
|
|
assert!(!is_disjoint(
|
|
"python_full_version == '3.7.*'",
|
|
"python_full_version == '3.7.1'"
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
"python_version == '3.7'",
|
|
"python_full_version == '3.8'"
|
|
));
|
|
|
|
assert!(!is_disjoint(
|
|
"python_version == '3.7'",
|
|
"python_full_version == '3.7.2'"
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
"python_version > '3.7'",
|
|
"python_full_version == '3.7.1'"
|
|
));
|
|
|
|
assert!(!is_disjoint(
|
|
"python_version <= '3.7'",
|
|
"python_full_version == '3.7.1'"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_disjointness() {
|
|
assert!(!is_disjoint(
|
|
"os_name == 'Linux'",
|
|
"platform_version == '3.7.1'"
|
|
));
|
|
assert!(!is_disjoint(
|
|
"implementation_version == '3.7.0'",
|
|
"python_full_version == '3.7.1'"
|
|
));
|
|
|
|
// basic version bounds checking should still work with lexicographical comparisons
|
|
test_version_bounds_disjointness("platform_version");
|
|
|
|
assert!(is_disjoint("os_name == 'Linux'", "os_name == 'OSX'"));
|
|
assert!(is_disjoint("os_name <= 'Linux'", "os_name == 'OSX'"));
|
|
|
|
assert!(!is_disjoint(
|
|
"os_name in 'OSXLinuxWindows'",
|
|
"os_name == 'OSX'"
|
|
));
|
|
assert!(!is_disjoint("'OSX' in os_name", "'Linux' in os_name"));
|
|
|
|
// complicated `in` intersections are not supported
|
|
assert!(!is_disjoint("os_name in 'OSX'", "os_name in 'Linux'"));
|
|
assert!(!is_disjoint(
|
|
"os_name in 'OSXLinux'",
|
|
"os_name == 'Windows'"
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
"os_name in 'Windows'",
|
|
"os_name not in 'Windows'"
|
|
));
|
|
assert!(is_disjoint(
|
|
"'Windows' in os_name",
|
|
"'Windows' not in os_name"
|
|
));
|
|
|
|
assert!(!is_disjoint("'Windows' in os_name", "'Windows' in os_name"));
|
|
assert!(!is_disjoint("'Linux' in os_name", "os_name not in 'Linux'"));
|
|
assert!(!is_disjoint("'Linux' not in os_name", "os_name in 'Linux'"));
|
|
|
|
assert!(!is_disjoint(
|
|
"os_name == 'Linux' and os_name != 'OSX'",
|
|
"os_name == 'Linux'"
|
|
));
|
|
assert!(is_disjoint(
|
|
"os_name == 'Linux' and os_name != 'OSX'",
|
|
"os_name == 'OSX'"
|
|
));
|
|
|
|
assert!(!is_disjoint(
|
|
"extra == 'Linux' and extra != 'OSX'",
|
|
"extra == 'Linux'"
|
|
));
|
|
assert!(is_disjoint(
|
|
"extra == 'Linux' and extra != 'OSX'",
|
|
"extra == 'OSX'"
|
|
));
|
|
|
|
assert!(!is_disjoint(
|
|
"extra == 'x1' and extra != 'x2'",
|
|
"extra == 'x1'"
|
|
));
|
|
assert!(is_disjoint(
|
|
"extra == 'x1' and extra != 'x2'",
|
|
"extra == 'x2'"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn is_disjoint_commutative() {
|
|
let m1 = m("extra == 'Linux' and extra != 'OSX'");
|
|
let m2 = m("extra == 'Linux'");
|
|
assert!(!m2.is_disjoint(&m1));
|
|
assert!(!m1.is_disjoint(&m2));
|
|
}
|
|
|
|
#[test]
|
|
fn test_combined_disjointness() {
|
|
assert!(!is_disjoint(
|
|
"os_name == 'a' and platform_version == '1'",
|
|
"os_name == 'a'"
|
|
));
|
|
assert!(!is_disjoint(
|
|
"os_name == 'a' or platform_version == '1'",
|
|
"os_name == 'a'"
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
"os_name == 'a' and platform_version == '1'",
|
|
"os_name == 'a' and platform_version == '2'"
|
|
));
|
|
assert!(is_disjoint(
|
|
"os_name == 'a' and platform_version == '1'",
|
|
"'2' == platform_version and os_name == 'a'"
|
|
));
|
|
assert!(!is_disjoint(
|
|
"os_name == 'a' or platform_version == '1'",
|
|
"os_name == 'a' or platform_version == '2'"
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
"sys_platform == 'darwin' and implementation_name == 'pypy'",
|
|
"sys_platform == 'bar' or implementation_name == 'foo'",
|
|
));
|
|
assert!(is_disjoint(
|
|
"sys_platform == 'bar' or implementation_name == 'foo'",
|
|
"sys_platform == 'darwin' and implementation_name == 'pypy'",
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
"python_version >= '3.7' and implementation_name == 'pypy'",
|
|
"python_version < '3.7'"
|
|
));
|
|
assert!(is_disjoint(
|
|
"implementation_name == 'pypy' and python_version >= '3.7'",
|
|
"implementation_name != 'pypy'"
|
|
));
|
|
assert!(is_disjoint(
|
|
"implementation_name != 'pypy' and python_version >= '3.7'",
|
|
"implementation_name == 'pypy'"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_arbitrary() {
|
|
assert!(m("'wat' == 'wat'").is_true());
|
|
assert!(m("os_name ~= 'wat'").is_true());
|
|
assert!(m("python_version == 'Linux'").is_true());
|
|
assert!(m("os_name ~= 'wat' or 'wat' == 'wat' and python_version == 'Linux'").is_true());
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_false() {
|
|
assert!(m("python_version < '3.10' and python_version >= '3.10'").is_false());
|
|
assert!(m("(python_version < '3.10' and python_version >= '3.10') \
|
|
or (python_version < '3.9' and python_version >= '3.9')",)
|
|
.is_false());
|
|
|
|
assert!(!m("python_version < '3.10'").is_false());
|
|
assert!(!m("python_version < '0'").is_false());
|
|
assert!(!m("python_version < '3.10' and python_version >= '3.9'").is_false());
|
|
assert!(!m("python_version < '3.10' or python_version >= '3.11'").is_false());
|
|
}
|
|
|
|
fn test_version_bounds_disjointness(version: &str) {
|
|
assert!(!is_disjoint(
|
|
format!("{version} > '2.7.0'"),
|
|
format!("{version} == '3.6.0'")
|
|
));
|
|
assert!(!is_disjoint(
|
|
format!("{version} >= '3.7.0'"),
|
|
format!("{version} == '3.7.1'")
|
|
));
|
|
assert!(!is_disjoint(
|
|
format!("{version} >= '3.7.0'"),
|
|
format!("'3.7.1' == {version}")
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
format!("{version} >= '3.7.1'"),
|
|
format!("{version} == '3.7.0'")
|
|
));
|
|
assert!(is_disjoint(
|
|
format!("'3.7.1' <= {version}"),
|
|
format!("{version} == '3.7.0'")
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
format!("{version} < '3.7.0'"),
|
|
format!("{version} == '3.7.0'")
|
|
));
|
|
assert!(is_disjoint(
|
|
format!("'3.7.0' > {version}"),
|
|
format!("{version} == '3.7.0'")
|
|
));
|
|
assert!(is_disjoint(
|
|
format!("{version} < '3.7.0'"),
|
|
format!("{version} == '3.7.1'")
|
|
));
|
|
|
|
assert!(is_disjoint(
|
|
format!("{version} == '3.7.0'"),
|
|
format!("{version} == '3.7.1'")
|
|
));
|
|
assert!(is_disjoint(
|
|
format!("{version} == '3.7.0'"),
|
|
format!("{version} != '3.7.0'")
|
|
));
|
|
}
|
|
|
|
fn assert_simplifies(left: &str, right: &str) {
|
|
assert_eq!(m(left), m(right), "{left} != {right}");
|
|
assert_eq!(m(left).try_to_string().unwrap(), right, "{left} != {right}");
|
|
}
|
|
|
|
fn assert_true(marker: &str) {
|
|
assert!(m(marker).is_true(), "{marker} != true");
|
|
}
|
|
|
|
fn assert_false(marker: &str) {
|
|
assert!(m(marker).is_false(), "{marker} != false");
|
|
}
|
|
|
|
fn is_disjoint(left: impl AsRef<str>, right: impl AsRef<str>) -> bool {
|
|
let (left, right) = (m(left.as_ref()), m(right.as_ref()));
|
|
left.is_disjoint(&right) && right.is_disjoint(&left)
|
|
}
|
|
|
|
fn implies(antecedent: &str, consequent: &str) -> bool {
|
|
let mut marker = m(antecedent);
|
|
marker.implies(m(consequent));
|
|
marker.is_true()
|
|
}
|
|
|
|
#[test]
|
|
fn complexified_markers() {
|
|
// Takes optional lower (inclusive) and upper (exclusive)
|
|
// bounds representing `requires-python` and a "simplified"
|
|
// marker, and returns the "complexified" marker. That is, a
|
|
// marker that embeds the `requires-python` constraint into it.
|
|
let complexify =
|
|
|lower: Option<[u64; 2]>, upper: Option<[u64; 2]>, marker: &str| -> MarkerTree {
|
|
let lower = lower
|
|
.map(|release| Bound::Included(Version::new(release)))
|
|
.unwrap_or(Bound::Unbounded);
|
|
let upper = upper
|
|
.map(|release| Bound::Excluded(Version::new(release)))
|
|
.unwrap_or(Bound::Unbounded);
|
|
m(marker).complexify_python_versions(lower.as_ref(), upper.as_ref())
|
|
};
|
|
|
|
assert_eq!(
|
|
complexify(None, None, "python_full_version < '3.10'"),
|
|
m("python_full_version < '3.10'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(Some([3, 8]), None, "python_full_version < '3.10'"),
|
|
m("python_full_version >= '3.8' and python_full_version < '3.10'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(None, Some([3, 8]), "python_full_version < '3.10'"),
|
|
m("python_full_version < '3.8'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(Some([3, 8]), Some([3, 8]), "python_full_version < '3.10'"),
|
|
// Kinda weird, but this normalizes to `false`, just like the above.
|
|
m("python_full_version < '0' and python_full_version > '0'"),
|
|
);
|
|
|
|
assert_eq!(
|
|
complexify(Some([3, 11]), None, "python_full_version < '3.10'"),
|
|
// Kinda weird, but this normalizes to `false`, just like the above.
|
|
m("python_full_version < '0' and python_full_version > '0'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(Some([3, 11]), None, "python_full_version >= '3.10'"),
|
|
m("python_full_version >= '3.11'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(Some([3, 11]), None, "python_full_version >= '3.12'"),
|
|
m("python_full_version >= '3.12'"),
|
|
);
|
|
|
|
assert_eq!(
|
|
complexify(None, Some([3, 11]), "python_full_version > '3.12'"),
|
|
// Kinda weird, but this normalizes to `false`, just like the above.
|
|
m("python_full_version < '0' and python_full_version > '0'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(None, Some([3, 11]), "python_full_version <= '3.12'"),
|
|
m("python_full_version < '3.11'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(None, Some([3, 11]), "python_full_version <= '3.10'"),
|
|
m("python_full_version <= '3.10'"),
|
|
);
|
|
|
|
assert_eq!(
|
|
complexify(Some([3, 11]), None, "python_full_version == '3.8'"),
|
|
// Kinda weird, but this normalizes to `false`, just like the above.
|
|
m("python_full_version < '0' and python_full_version > '0'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(
|
|
Some([3, 11]),
|
|
None,
|
|
"python_full_version == '3.8' or python_full_version == '3.12'"
|
|
),
|
|
m("python_full_version == '3.12'"),
|
|
);
|
|
assert_eq!(
|
|
complexify(
|
|
Some([3, 11]),
|
|
None,
|
|
"python_full_version == '3.8' \
|
|
or python_full_version == '3.11' \
|
|
or python_full_version == '3.12'"
|
|
),
|
|
m("python_full_version == '3.11' or python_full_version == '3.12'"),
|
|
);
|
|
|
|
// Tests a tricky case where if a marker is always true, then
|
|
// complexifying it will proceed correctly by adding the
|
|
// requires-python constraint. This is a regression test for
|
|
// an early implementation that special cased the "always
|
|
// true" case to return "always true" regardless of the
|
|
// requires-python bounds.
|
|
assert_eq!(
|
|
complexify(
|
|
Some([3, 12]),
|
|
None,
|
|
"python_full_version < '3.10' or python_full_version >= '3.10'"
|
|
),
|
|
m("python_full_version >= '3.12'"),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn simplified_markers() {
|
|
// Takes optional lower (inclusive) and upper (exclusive)
|
|
// bounds representing `requires-python` and a "complexified"
|
|
// marker, and returns the "simplified" marker. That is, a
|
|
// marker that assumes `requires-python` is true.
|
|
let simplify =
|
|
|lower: Option<[u64; 2]>, upper: Option<[u64; 2]>, marker: &str| -> MarkerTree {
|
|
let lower = lower
|
|
.map(|release| Bound::Included(Version::new(release)))
|
|
.unwrap_or(Bound::Unbounded);
|
|
let upper = upper
|
|
.map(|release| Bound::Excluded(Version::new(release)))
|
|
.unwrap_or(Bound::Unbounded);
|
|
m(marker).simplify_python_versions(lower.as_ref(), upper.as_ref())
|
|
};
|
|
|
|
assert_eq!(
|
|
simplify(
|
|
Some([3, 8]),
|
|
None,
|
|
"python_full_version >= '3.8' and python_full_version < '3.10'"
|
|
),
|
|
m("python_full_version < '3.10'"),
|
|
);
|
|
assert_eq!(
|
|
simplify(Some([3, 8]), None, "python_full_version < '3.7'"),
|
|
// Kinda weird, but this normalizes to `false`, just like the above.
|
|
m("python_full_version < '0' and python_full_version > '0'"),
|
|
);
|
|
assert_eq!(
|
|
simplify(
|
|
Some([3, 8]),
|
|
Some([3, 11]),
|
|
"python_full_version == '3.7.*' \
|
|
or python_full_version == '3.8.*' \
|
|
or python_full_version == '3.10.*' \
|
|
or python_full_version == '3.11.*' \
|
|
"
|
|
),
|
|
// Given `requires-python = '>=3.8,<3.11'`, only `3.8.*`
|
|
// and `3.10.*` can possibly be true. So this simplifies
|
|
// to `!= 3.9.*`.
|
|
m("python_full_version != '3.9.*'"),
|
|
);
|
|
assert_eq!(
|
|
simplify(
|
|
Some([3, 8]),
|
|
None,
|
|
"python_full_version >= '3.8' and sys_platform == 'win32'"
|
|
),
|
|
m("sys_platform == 'win32'"),
|
|
);
|
|
assert_eq!(
|
|
simplify(
|
|
Some([3, 8]),
|
|
None,
|
|
"python_full_version >= '3.9' \
|
|
and (sys_platform == 'win32' or python_full_version >= '3.8')",
|
|
),
|
|
m("python_full_version >= '3.9'"),
|
|
);
|
|
}
|
|
}
|