use std::cmp::Ordering;
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::ops::{Bound, Deref};
use std::str::FromStr;
use itertools::Itertools;
use pep440_rs::{Version, VersionParseError, VersionSpecifier};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use version_ranges::Ranges;
use crate::cursor::Cursor;
use crate::marker::parse;
use crate::{
ExtraName, MarkerEnvironment, Pep508Error, Pep508ErrorSource, Pep508Url, Reporter,
TracingReporter,
};
use super::algebra::{Edges, NodeId, Variable, INTERNER};
use super::simplify;
#[derive(Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Clone, Copy)]
pub enum MarkerWarningKind {
DeprecatedMarkerName,
ExtraInvalidComparison,
LexicographicComparison,
MarkerMarkerComparison,
Pep440Error,
StringStringComparison,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[allow(clippy::enum_variant_names)]
pub enum MarkerValueVersion {
ImplementationVersion,
PythonFullVersion,
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"),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum MarkerValueString {
ImplementationName,
OsName,
OsNameDeprecated,
PlatformMachine,
PlatformMachineDeprecated,
PlatformPythonImplementation,
PlatformPythonImplementationDeprecated,
PythonImplementationDeprecated,
PlatformRelease,
PlatformSystem,
PlatformVersion,
PlatformVersionDeprecated,
SysPlatform,
SysPlatformDeprecated,
}
impl Display for MarkerValueString {
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"),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum MarkerValue {
MarkerEnvVersion(MarkerValueVersion),
MarkerEnvString(MarkerValueString),
Extra,
QuotedString(String),
}
impl FromStr for MarkerValue {
type Err = String;
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}'"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum MarkerOperator {
Equal,
NotEqual,
GreaterThan,
GreaterEqual,
LessThan,
LessEqual,
TildeEqual,
In,
NotIn,
Contains,
NotContains,
}
impl MarkerOperator {
pub(crate) fn to_pep440_operator(self) -> Option<pep440_rs::Operator> {
match self {
Self::Equal => Some(pep440_rs::Operator::Equal),
Self::NotEqual => Some(pep440_rs::Operator::NotEqual),
Self::GreaterThan => Some(pep440_rs::Operator::GreaterThan),
Self::GreaterEqual => Some(pep440_rs::Operator::GreaterThanEqual),
Self::LessThan => Some(pep440_rs::Operator::LessThan),
Self::LessEqual => Some(pep440_rs::Operator::LessThanEqual),
Self::TildeEqual => Some(pep440_rs::Operator::TildeEqual),
_ => None,
}
}
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,
}
}
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,
})
}
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)
}
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,
}
}
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;
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
.strip_prefix("not")
.and_then(|space_in| space_in.strip_suffix("in"))
.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",
})
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct StringVersion {
pub string: String,
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
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum MarkerValueExtra {
Extra(ExtraName),
Arbitrary(String),
}
impl MarkerValueExtra {
fn as_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),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[allow(missing_docs)]
pub enum MarkerExpression {
Version {
key: MarkerValueVersion,
specifier: VersionSpecifier,
},
VersionIn {
key: MarkerValueVersion,
versions: Vec<Version>,
negated: bool,
},
String {
key: MarkerValueString,
operator: MarkerOperator,
value: String,
},
Extra {
operator: ExtraOperator,
name: MarkerValueExtra,
},
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum ExtraOperator {
Equal,
NotEqual,
}
impl ExtraOperator {
pub(crate) fn from_marker_operator(operator: MarkerOperator) -> Option<ExtraOperator> {
match operator {
MarkerOperator::Equal => Some(ExtraOperator::Equal),
MarkerOperator::NotEqual => Some(ExtraOperator::NotEqual),
_ => None,
}
}
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 {
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)
}
#[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 == &pep440_rs::Operator::EqualStar || op == &pep440_rs::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}'")
}
}
}
}
#[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 {
pub fn parse_str<T: Pep508Url>(markers: &str) -> Result<Self, Pep508Error<T>> {
parse::parse_markers(markers, &mut TracingReporter)
}
pub fn parse_reporter(
markers: &str,
reporter: &mut impl Reporter,
) -> Result<Self, Pep508Error> {
parse::parse_markers(markers, reporter)
}
pub const TRUE: MarkerTree = MarkerTree(NodeId::TRUE);
pub const FALSE: MarkerTree = MarkerTree(NodeId::FALSE);
pub fn expression(expr: MarkerExpression) -> MarkerTree {
MarkerTree(INTERNER.lock().expression(expr))
}
pub fn is_true(&self) -> bool {
self.0.is_true()
}
pub fn is_false(&self) -> bool {
self.0.is_false()
}
#[must_use]
pub fn negate(&self) -> MarkerTree {
MarkerTree(self.0.not())
}
#[allow(clippy::needless_pass_by_value)]
pub fn and(&mut self, tree: MarkerTree) {
self.0 = INTERNER.lock().and(self.0, tree.0);
}
#[allow(clippy::needless_pass_by_value)]
pub fn or(&mut self, tree: MarkerTree) {
self.0 = INTERNER.lock().or(self.0, tree.0);
}
pub fn is_disjoint(&self, other: &MarkerTree) -> bool {
INTERNER.lock().is_disjoint(self.0, other.0)
}
pub fn contents(&self) -> Option<MarkerTreeContents> {
if self.is_true() {
return None;
}
Some(MarkerTreeContents(self.clone()))
}
pub fn try_to_string(&self) -> Option<String> {
self.contents().map(|contents| contents.to_string())
}
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.clone(),
map,
})
}
Variable::String(key) => {
let Edges::String { edges: ref map } = node.children else {
unreachable!()
};
MarkerTreeKind::String(StringMarkerTree {
id: self.0,
key: key.clone(),
map,
})
}
Variable::In { key, value } => {
let Edges::Boolean { low, high } = node.children else {
unreachable!()
};
MarkerTreeKind::In(InMarkerTree {
key: key.clone(),
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.clone(),
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),
})
}
}
}
pub fn to_dnf(&self) -> Vec<Vec<MarkerExpression>> {
simplify::to_dnf(self)
}
pub fn evaluate(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
self.report_deprecated_options(&mut TracingReporter);
self.evaluate_reporter_impl(env, extras, &mut TracingReporter)
}
pub fn evaluate_optional_environment(
&self,
env: Option<&MarkerEnvironment>,
extras: &[ExtraName],
) -> bool {
self.report_deprecated_options(&mut TracingReporter);
match env {
None => self.evaluate_extras(extras),
Some(env) => self.evaluate_reporter_impl(env, extras, &mut TracingReporter),
}
}
pub fn evaluate_reporter(
&self,
env: &MarkerEnvironment,
extras: &[ExtraName],
reporter: &mut impl Reporter,
) -> bool {
self.report_deprecated_options(reporter);
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"),
);
};
}
}
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(
marker
.name()
.as_extra()
.is_some_and(|extra| extras.contains(extra)),
)
.evaluate_reporter_impl(env, extras, reporter);
}
}
false
}
pub fn evaluate_extras_and_python_version(
&self,
extras: &HashSet<ExtraName>,
python_versions: &[Version],
) -> bool {
match self.kind() {
MarkerTreeKind::True => true,
MarkerTreeKind::False => false,
MarkerTreeKind::Version(marker) => marker.edges().any(|(range, tree)| {
if *marker.key() == MarkerValueVersion::PythonVersion {
if !python_versions
.iter()
.any(|version| range.contains(version))
{
return false;
}
}
tree.evaluate_extras_and_python_version(extras, python_versions)
}),
MarkerTreeKind::String(marker) => marker
.children()
.any(|(_, tree)| tree.evaluate_extras_and_python_version(extras, python_versions)),
MarkerTreeKind::In(marker) => marker
.children()
.any(|(_, tree)| tree.evaluate_extras_and_python_version(extras, python_versions)),
MarkerTreeKind::Contains(marker) => marker
.children()
.any(|(_, tree)| tree.evaluate_extras_and_python_version(extras, python_versions)),
MarkerTreeKind::Extra(marker) => marker
.edge(
marker
.name()
.as_extra()
.is_some_and(|extra| extras.contains(extra)),
)
.evaluate_extras_and_python_version(extras, python_versions),
}
}
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(
marker
.name()
.as_extra()
.is_some_and(|extra| extras.contains(extra)),
)
.evaluate_extras(extras),
}
}
pub fn evaluate_collect_warnings(
&self,
env: &MarkerEnvironment,
extras: &[ExtraName],
) -> (bool, Vec<(MarkerWarningKind, String)>) {
let mut warnings = Vec::new();
let mut reporter = |kind, warning| {
warnings.push((kind, warning));
};
self.report_deprecated_options(&mut reporter);
let result = self.evaluate_reporter_impl(env, extras, &mut reporter);
(result, warnings)
}
fn report_deprecated_options(&self, reporter: &mut impl Reporter) {
let string_marker = match self.kind() {
MarkerTreeKind::True | MarkerTreeKind::False => return,
MarkerTreeKind::String(marker) => marker,
MarkerTreeKind::Version(marker) => {
for (_, tree) in marker.edges() {
tree.report_deprecated_options(reporter);
}
return;
}
MarkerTreeKind::In(marker) => {
for (_, tree) in marker.children() {
tree.report_deprecated_options(reporter);
}
return;
}
MarkerTreeKind::Contains(marker) => {
for (_, tree) in marker.children() {
tree.report_deprecated_options(reporter);
}
return;
}
MarkerTreeKind::Extra(marker) => {
for (_, tree) in marker.children() {
tree.report_deprecated_options(reporter);
}
return;
}
};
match string_marker.key() {
MarkerValueString::OsNameDeprecated => {
reporter.report(
MarkerWarningKind::DeprecatedMarkerName,
"os.name is deprecated in favor of os_name".to_string(),
);
}
MarkerValueString::PlatformMachineDeprecated => {
reporter.report(
MarkerWarningKind::DeprecatedMarkerName,
"platform.machine is deprecated in favor of platform_machine".to_string(),
);
}
MarkerValueString::PlatformPythonImplementationDeprecated => {
reporter.report(
MarkerWarningKind::DeprecatedMarkerName,
"platform.python_implementation is deprecated in favor of
platform_python_implementation"
.to_string(),
);
}
MarkerValueString::PythonImplementationDeprecated => {
reporter.report(
MarkerWarningKind::DeprecatedMarkerName,
"python_implementation is deprecated in favor of
platform_python_implementation"
.to_string(),
);
}
MarkerValueString::PlatformVersionDeprecated => {
reporter.report(
MarkerWarningKind::DeprecatedMarkerName,
"platform.version is deprecated in favor of platform_version".to_string(),
);
}
MarkerValueString::SysPlatformDeprecated => {
reporter.report(
MarkerWarningKind::DeprecatedMarkerName,
"sys.platform is deprecated in favor of sys_platform".to_string(),
);
}
_ => {}
}
for (_, tree) in string_marker.children() {
tree.report_deprecated_options(reporter);
}
}
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,
..
}
)
})?;
if let Some(ref extra_expression) = extra_expression {
if *extra_expression != *found {
return None;
}
continue;
}
extra_expression = Some(found.clone());
}
extra_expression
}
#[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),
)
}
#[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),
)
}
#[must_use]
pub fn simplify_extras(self, extras: &[ExtraName]) -> MarkerTree {
self.simplify_extras_with(|name| extras.contains(name))
}
#[must_use]
pub fn simplify_extras_with(self, is_extra: impl Fn(&ExtraName) -> bool) -> MarkerTree {
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) => name
.as_extra()
.and_then(|name| is_extra(name).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 {
pub fn debug_graph(&self) -> MarkerTreeDebugGraph<'_> {
MarkerTreeDebugGraph { marker: self }
}
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())
}
}
#[derive(Clone)]
pub struct MarkerTreeDebugGraph<'a> {
marker: &'a MarkerTree,
}
impl fmt::Debug for MarkerTreeDebugGraph<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.marker.fmt_graph(f, 0)
}
}
#[derive(Clone)]
pub struct MarkerTreeDebugRaw<'a> {
marker: &'a MarkerTree,
}
impl fmt::Debug for MarkerTreeDebugRaw<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let node = INTERNER.shared.node(self.marker.0);
f.debug_tuple("MarkerTreeDebugRaw").field(node).finish()
}
}
#[derive(PartialEq, Eq, Clone, Debug, PartialOrd, Ord)]
pub enum MarkerTreeKind<'a> {
True,
False,
Version(VersionMarkerTree<'a>),
String(StringMarkerTree<'a>),
In(InMarkerTree<'a>),
Contains(ContainsMarkerTree<'a>),
Extra(ExtraMarkerTree<'a>),
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct VersionMarkerTree<'a> {
id: NodeId,
key: MarkerValueVersion,
map: &'a [(Ranges<Version>, NodeId)],
}
impl VersionMarkerTree<'_> {
pub fn key(&self) -> &MarkerValueVersion {
&self.key
}
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()))
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct StringMarkerTree<'a> {
id: NodeId,
key: MarkerValueString,
map: &'a [(Ranges<String>, NodeId)],
}
impl StringMarkerTree<'_> {
pub fn key(&self) -> &MarkerValueString {
&self.key
}
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()))
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct InMarkerTree<'a> {
key: MarkerValueString,
value: &'a str,
high: NodeId,
low: NodeId,
}
impl InMarkerTree<'_> {
pub fn key(&self) -> &MarkerValueString {
&self.key
}
pub fn value(&self) -> &str {
self.value
}
pub fn children(&self) -> impl Iterator<Item = (bool, MarkerTree)> {
[(true, MarkerTree(self.high)), (false, MarkerTree(self.low))].into_iter()
}
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()))
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ContainsMarkerTree<'a> {
key: MarkerValueString,
value: &'a str,
high: NodeId,
low: NodeId,
}
impl ContainsMarkerTree<'_> {
pub fn key(&self) -> &MarkerValueString {
&self.key
}
pub fn value(&self) -> &str {
self.value
}
pub fn children(&self) -> impl Iterator<Item = (bool, MarkerTree)> {
[(true, MarkerTree(self.high)), (false, MarkerTree(self.low))].into_iter()
}
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()))
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ExtraMarkerTree<'a> {
name: &'a MarkerValueExtra,
high: NodeId,
low: NodeId,
}
impl ExtraMarkerTree<'_> {
pub fn name(&self) -> &MarkerValueExtra {
self.name
}
pub fn children(&self) -> impl Iterator<Item = (bool, MarkerTree)> {
[(true, MarkerTree(self.high)), (false, MarkerTree(self.low))].into_iter()
}
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()))
}
}
#[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 {
if self.0.is_false() {
return write!(f, "python_version < '0'");
}
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 pep440_rs::Version;
use crate::marker::{MarkerEnvironment, MarkerEnvironmentBuilder};
use crate::{ExtraName, 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()
}
#[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'"
);
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: pep508_rs: os.name is deprecated in favor of os_name",
"WARN warnings4: pep508_rs: platform.machine is deprecated in favor of platform_machine",
"WARN warnings4: pep508_rs: platform.python_implementation is deprecated in favor of",
"WARN warnings4: pep508_rs: sys.platform is deprecated in favor of sys_platform",
"WARN warnings4: pep508_rs: 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, warnings) = MarkerTree::from_str("python_version > '3.6'")
.unwrap()
.evaluate_collect_warnings(&env37, &[]);
assert_eq!(warnings, &[]);
assert!(result);
let (result, warnings) = MarkerTree::from_str("'3.6' > python_version")
.unwrap()
.evaluate_collect_warnings(&env37, &[]);
assert_eq!(warnings, &[]);
assert!(!result);
let (result, warnings) = MarkerTree::from_str("'3.*' == python_version")
.unwrap()
.evaluate_collect_warnings(&env37, &[]);
assert_eq!(warnings, &[]);
assert!(result);
}
#[test]
fn test_marker_string_inverted() {
let env37 = env37();
let (result, warnings) = MarkerTree::from_str("'nux' in sys_platform")
.unwrap()
.evaluate_collect_warnings(&env37, &[]);
assert_eq!(warnings, &[]);
assert!(result);
let (result, warnings) = MarkerTree::from_str("sys_platform in 'nux'")
.unwrap()
.evaluate_collect_warnings(&env37, &[]);
assert_eq!(warnings, &[]);
assert!(!result);
}
#[test]
fn test_marker_version_star() {
let env37 = env37();
let (result, warnings) = MarkerTree::from_str("python_version == '3.7.*'")
.unwrap()
.evaluate_collect_warnings(&env37, &[]);
assert_eq!(warnings, &[]);
assert!(result);
}
#[test]
fn test_tilde_equal() {
let env37 = env37();
let (result, warnings) = MarkerTree::from_str("python_version ~= '3.7'")
.unwrap()
.evaluate_collect_warnings(&env37, &[]);
assert_eq!(warnings, &[]);
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() {
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);
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);
let markers = MarkerTree::from_str(r#"extra == "dev""#).unwrap();
let simplified = markers.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
assert_eq!(simplified, MarkerTree::TRUE);
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);
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);
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);
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'");
assert_false("python_version in '3.9.0'");
assert_true("python_version in 'foo'");
assert_true("python_version in '3.9.*'");
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'");
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.*'",
);
assert_simplifies(
"python_version in '3.9 3.11'",
"python_full_version == '3.9.*' or python_full_version == '3.11.*'",
);
assert_simplifies(
"python_version in '3.9 3.10 3.11'",
"python_full_version >= '3.9' and python_full_version < '3.12'",
);
assert_simplifies(
"implementation_version in '3.9 3.11'",
"implementation_version == '3.9' or implementation_version == '3.11'",
);
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'",
);
assert_simplifies(
"python_version not in '3.9 3.10 3.11'",
"python_full_version < '3.9' or python_full_version >= '3.12'",
);
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'",
);
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());
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'",
);
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'",
);
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'",
);
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'",
);
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'",
);
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());
}
#[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'")
);
assert_eq!(m("sys_platform ~= 'linux'").negate(), MarkerTree::FALSE);
assert_eq!(m("'foo' == 'bar'").negate(), MarkerTree::FALSE);
assert_eq!(
m("os_name == 'bar' and os_name == 'foo'").negate(),
m("os_name != 'bar' or os_name != 'foo'")
);
assert_eq!(
m("os_name == 'bar' or os_name == 'foo'").negate(),
m("os_name != 'bar' and os_name != 'foo'")
);
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() {
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')",
);
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'",
);
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() {
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'"
));
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"));
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)
}
#[test]
fn complexified_markers() {
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'"),
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 < '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'"),
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'"),
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'"),
);
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() {
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'"),
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.*' \
"
),
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'"),
);
}
}