Files
uv/crates/pep508-rs/src/marker.rs
T
Charlie Marsh ac87fd4006 Disable Clippy's too-many-arguments rule (#4663)
## Summary

We allow this constantly, I think it's just too pedantic for us.
2024-06-30 19:30:38 +00:00

2585 lines
96 KiB
Rust

//! PEP 508 markers implementations with validation and warnings
//!
//! Markers allow you to install dependencies only in specific environments (python version,
//! operating system, architecture, etc.) or when a specific feature is activated. E.g. you can
//! say `importlib-metadata ; python_version < "3.8"` or
//! `itsdangerous (>=1.1.0) ; extra == 'security'`. Unfortunately, the marker grammar has some
//! oversights (e.g. <https://github.com/pypa/packaging.python.org/pull/1181>) and
//! the design of comparisons (PEP 440 comparisons with lexicographic fallback) leads to confusing
//! outcomes. This implementation tries to carefully validate everything and emit warnings whenever
//! bogus comparisons with unintended semantics are made.
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
#[cfg(feature = "pyo3")]
use pyo3::{
basic::CompareOp, exceptions::PyValueError, pyclass, pymethods, types::PyAnyMethods, PyResult,
Python,
};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use pep440_rs::{Version, VersionParseError, VersionPattern, VersionSpecifier};
use uv_normalize::ExtraName;
use crate::cursor::Cursor;
use crate::{Pep508Error, Pep508ErrorSource, Pep508Url, Reporter, TracingReporter};
/// Ways in which marker evaluation can fail
#[derive(Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))]
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,
}
#[cfg(feature = "pyo3")]
#[pymethods]
impl MarkerWarningKind {
#[allow(clippy::trivially_copy_pass_by_ref)]
fn __hash__(&self) -> u8 {
*self as u8
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn __richcmp__(&self, other: Self, op: CompareOp) -> bool {
op.matches(self.cmp(&other))
}
}
/// Those environment markers with a PEP 440 version as value such as `python_version`
#[derive(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(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 MarkerValue {
fn string_value(value: String) -> Self {
Self::QuotedString(value)
}
}
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,
}
impl MarkerOperator {
/// Compare two versions, returning None for `in` and `not in`
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),
Self::In => None,
Self::NotIn => 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 => "in",
Self::NotIn => "not in",
})
}
}
/// Helper type with a [Version] and its original text
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(get_all, module = "pep508"))]
pub struct StringVersion {
/// Original unchanged string
pub string: String,
/// Parsed version
pub version: 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 marker values for a python interpreter, normally the current one
///
/// <https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers>
///
/// Some are `(String, Version)` because we have to support version comparison
#[allow(missing_docs, clippy::unsafe_derive_deserialize)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))]
pub struct MarkerEnvironment {
#[serde(flatten)]
inner: Arc<MarkerEnvironmentInner>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
struct MarkerEnvironmentInner {
implementation_name: String,
implementation_version: StringVersion,
os_name: String,
platform_machine: String,
platform_python_implementation: String,
platform_release: String,
platform_system: String,
platform_version: String,
python_full_version: StringVersion,
python_version: StringVersion,
sys_platform: String,
}
impl MarkerEnvironment {
/// Returns of the PEP 440 version typed value of the key in the current environment
pub fn get_version(&self, key: &MarkerValueVersion) -> &Version {
match key {
MarkerValueVersion::ImplementationVersion => &self.implementation_version().version,
MarkerValueVersion::PythonFullVersion => &self.python_full_version().version,
MarkerValueVersion::PythonVersion => &self.python_version().version,
}
}
/// Returns of the stringly typed value of the key in the current environment
pub fn get_string(&self, key: &MarkerValueString) -> &str {
match key {
MarkerValueString::ImplementationName => self.implementation_name(),
MarkerValueString::OsName | MarkerValueString::OsNameDeprecated => self.os_name(),
MarkerValueString::PlatformMachine | MarkerValueString::PlatformMachineDeprecated => {
self.platform_machine()
}
MarkerValueString::PlatformPythonImplementation
| MarkerValueString::PlatformPythonImplementationDeprecated
| MarkerValueString::PythonImplementationDeprecated => {
self.platform_python_implementation()
}
MarkerValueString::PlatformRelease => self.platform_release(),
MarkerValueString::PlatformSystem => self.platform_system(),
MarkerValueString::PlatformVersion | MarkerValueString::PlatformVersionDeprecated => {
self.platform_version()
}
MarkerValueString::SysPlatform | MarkerValueString::SysPlatformDeprecated => {
self.sys_platform()
}
}
}
}
/// APIs for retrieving specific parts of a marker environment.
impl MarkerEnvironment {
/// Returns the name of the Python implementation for this environment.
///
/// This is equivalent to `sys.implementation.name`.
///
/// Some example values are: `cpython`.
#[inline]
pub fn implementation_name(&self) -> &str {
&self.inner.implementation_name
}
/// Returns the Python implementation version for this environment.
///
/// This value is derived from `sys.implementation.version`. See [PEP 508
/// environment markers] for full details.
///
/// This is equivalent to `sys.implementation.name`.
///
/// Some example values are: `3.4.0`, `3.5.0b1`.
///
/// [PEP 508 environment markers]: https://peps.python.org/pep-0508/#environment-markers
#[inline]
pub fn implementation_version(&self) -> &StringVersion {
&self.inner.implementation_version
}
/// Returns the name of the operating system for this environment.
///
/// This is equivalent to `os.name`.
///
/// Some example values are: `posix`, `java`.
#[inline]
pub fn os_name(&self) -> &str {
&self.inner.os_name
}
/// Returns the name of the machine for this environment's platform.
///
/// This is equivalent to `platform.machine()`.
///
/// Some example values are: `x86_64`.
#[inline]
pub fn platform_machine(&self) -> &str {
&self.inner.platform_machine
}
/// Returns the name of the Python implementation for this environment's
/// platform.
///
/// This is equivalent to `platform.python_implementation()`.
///
/// Some example values are: `CPython`, `Jython`.
#[inline]
pub fn platform_python_implementation(&self) -> &str {
&self.inner.platform_python_implementation
}
/// Returns the release for this environment's platform.
///
/// This is equivalent to `platform.release()`.
///
/// Some example values are: `3.14.1-x86_64-linode39`, `14.5.0`, `1.8.0_51`.
#[inline]
pub fn platform_release(&self) -> &str {
&self.inner.platform_release
}
/// Returns the system for this environment's platform.
///
/// This is equivalent to `platform.system()`.
///
/// Some example values are: `Linux`, `Windows`, `Java`.
#[inline]
pub fn platform_system(&self) -> &str {
&self.inner.platform_system
}
/// Returns the version for this environment's platform.
///
/// This is equivalent to `platform.version()`.
///
/// Some example values are: `#1 SMP Fri Apr 25 13:07:35 EDT 2014`,
/// `Java HotSpot(TM) 64-Bit Server VM, 25.51-b03, Oracle Corporation`,
/// `Darwin Kernel Version 14.5.0: Wed Jul 29 02:18:53 PDT 2015;
/// root:xnu-2782.40.9~2/RELEASE_X86_64`.
#[inline]
pub fn platform_version(&self) -> &str {
&self.inner.platform_version
}
/// Returns the full version of Python for this environment.
///
/// This is equivalent to `platform.python_version()`.
///
/// Some example values are: `3.4.0`, `3.5.0b1`.
#[inline]
pub fn python_full_version(&self) -> &StringVersion {
&self.inner.python_full_version
}
/// Returns the version of Python for this environment.
///
/// This is equivalent to `'.'.join(platform.python_version_tuple()[:2])`.
///
/// Some example values are: `3.4`, `2.7`.
#[inline]
pub fn python_version(&self) -> &StringVersion {
&self.inner.python_version
}
/// Returns the name of the system platform for this environment.
///
/// This is equivalent to `sys.platform`.
///
/// Some example values are: `linux`, `linux2`, `darwin`, `java1.8.0_51`
/// (note that `linux` is from Python3 and `linux2` from Python2).
#[inline]
pub fn sys_platform(&self) -> &str {
&self.inner.sys_platform
}
}
/// APIs for setting specific parts of a marker environment.
impl MarkerEnvironment {
/// Set the name of the Python implementation for this environment.
///
/// See also [`MarkerEnvironment::implementation_name`].
#[inline]
#[must_use]
pub fn with_implementation_name(mut self, value: impl Into<String>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).implementation_name = value.into();
self
}
/// Set the Python implementation version for this environment.
///
/// See also [`MarkerEnvironment::implementation_version`].
#[inline]
#[must_use]
pub fn with_implementation_version(
mut self,
value: impl Into<StringVersion>,
) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).implementation_version = value.into();
self
}
/// Set the name of the operating system for this environment.
///
/// See also [`MarkerEnvironment::os_name`].
#[inline]
#[must_use]
pub fn with_os_name(mut self, value: impl Into<String>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).os_name = value.into();
self
}
/// Set the name of the machine for this environment's platform.
///
/// See also [`MarkerEnvironment::platform_machine`].
#[inline]
#[must_use]
pub fn with_platform_machine(mut self, value: impl Into<String>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).platform_machine = value.into();
self
}
/// Set the name of the Python implementation for this environment's
/// platform.
///
/// See also [`MarkerEnvironment::platform_python_implementation`].
#[inline]
#[must_use]
pub fn with_platform_python_implementation(
mut self,
value: impl Into<String>,
) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).platform_python_implementation = value.into();
self
}
/// Set the release for this environment's platform.
///
/// See also [`MarkerEnvironment::platform_release`].
#[inline]
#[must_use]
pub fn with_platform_release(mut self, value: impl Into<String>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).platform_release = value.into();
self
}
/// Set the system for this environment's platform.
///
/// See also [`MarkerEnvironment::platform_system`].
#[inline]
#[must_use]
pub fn with_platform_system(mut self, value: impl Into<String>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).platform_system = value.into();
self
}
/// Set the version for this environment's platform.
///
/// See also [`MarkerEnvironment::platform_version`].
#[inline]
#[must_use]
pub fn with_platform_version(mut self, value: impl Into<String>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).platform_version = value.into();
self
}
/// Set the full version of Python for this environment.
///
/// See also [`MarkerEnvironment::python_full_version`].
#[inline]
#[must_use]
pub fn with_python_full_version(
mut self,
value: impl Into<StringVersion>,
) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).python_full_version = value.into();
self
}
/// Set the version of Python for this environment.
///
/// See also [`MarkerEnvironment::python_full_version`].
#[inline]
#[must_use]
pub fn with_python_version(mut self, value: impl Into<StringVersion>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).python_version = value.into();
self
}
/// Set the name of the system platform for this environment.
///
/// See also [`MarkerEnvironment::sys_platform`].
#[inline]
#[must_use]
pub fn with_sys_platform(mut self, value: impl Into<String>) -> MarkerEnvironment {
Arc::make_mut(&mut self.inner).sys_platform = value.into();
self
}
}
#[cfg(feature = "pyo3")]
#[pymethods]
impl MarkerEnvironment {
/// Construct your own marker environment
#[new]
#[pyo3(signature = (*,
implementation_name,
implementation_version,
os_name,
platform_machine,
platform_python_implementation,
platform_release,
platform_system,
platform_version,
python_full_version,
python_version,
sys_platform
))]
fn py_new(
implementation_name: &str,
implementation_version: &str,
os_name: &str,
platform_machine: &str,
platform_python_implementation: &str,
platform_release: &str,
platform_system: &str,
platform_version: &str,
python_full_version: &str,
python_version: &str,
sys_platform: &str,
) -> PyResult<Self> {
let implementation_version =
StringVersion::from_str(implementation_version).map_err(|err| {
PyValueError::new_err(format!(
"implementation_version is not a valid PEP440 version: {err}"
))
})?;
let python_full_version = StringVersion::from_str(python_full_version).map_err(|err| {
PyValueError::new_err(format!(
"python_full_version is not a valid PEP440 version: {err}"
))
})?;
let python_version = StringVersion::from_str(python_version).map_err(|err| {
PyValueError::new_err(format!(
"python_version is not a valid PEP440 version: {err}"
))
})?;
Ok(Self {
inner: Arc::new(MarkerEnvironmentInner {
implementation_name: implementation_name.to_string(),
implementation_version,
os_name: os_name.to_string(),
platform_machine: platform_machine.to_string(),
platform_python_implementation: platform_python_implementation.to_string(),
platform_release: platform_release.to_string(),
platform_system: platform_system.to_string(),
platform_version: platform_version.to_string(),
python_full_version,
python_version,
sys_platform: sys_platform.to_string(),
}),
})
}
/// Query the current python interpreter to get the correct marker value
#[staticmethod]
fn current(py: Python<'_>) -> PyResult<Self> {
let os = py.import_bound("os")?;
let platform = py.import_bound("platform")?;
let sys = py.import_bound("sys")?;
let python_version_tuple: (String, String, String) = platform
.getattr("python_version_tuple")?
.call0()?
.extract()?;
// See pseudocode at
// https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers
let name = sys.getattr("implementation")?.getattr("name")?.extract()?;
let info = sys.getattr("implementation")?.getattr("version")?;
let kind = info.getattr("releaselevel")?.extract::<String>()?;
let implementation_version: String = format!(
"{}.{}.{}{}",
info.getattr("major")?.extract::<usize>()?,
info.getattr("minor")?.extract::<usize>()?,
info.getattr("micro")?.extract::<usize>()?,
if kind == "final" {
String::new()
} else {
format!("{}{}", kind, info.getattr("serial")?.extract::<usize>()?)
}
);
let python_full_version: String = platform.getattr("python_version")?.call0()?.extract()?;
let python_version = format!("{}.{}", python_version_tuple.0, python_version_tuple.1);
// This is not written down in PEP 508, but it's the only reasonable assumption to make
let implementation_version =
StringVersion::from_str(&implementation_version).map_err(|err| {
PyValueError::new_err(format!(
"Broken python implementation, implementation_version is not a valid PEP440 version: {err}"
))
})?;
let python_full_version = StringVersion::from_str(&python_full_version).map_err(|err| {
PyValueError::new_err(format!(
"Broken python implementation, python_full_version is not a valid PEP440 version: {err}"
))
})?;
let python_version = StringVersion::from_str(&python_version).map_err(|err| {
PyValueError::new_err(format!(
"Broken python implementation, python_version is not a valid PEP440 version: {err}"
))
})?;
Ok(Self {
inner: Arc::new(MarkerEnvironmentInner {
implementation_name: name,
implementation_version,
os_name: os.getattr("name")?.extract()?,
platform_machine: platform.getattr("machine")?.call0()?.extract()?,
platform_python_implementation: platform
.getattr("python_implementation")?
.call0()?
.extract()?,
platform_release: platform.getattr("release")?.call0()?.extract()?,
platform_system: platform.getattr("system")?.call0()?.extract()?,
platform_version: platform.getattr("version")?.call0()?.extract()?,
python_full_version,
python_version,
sys_platform: sys.getattr("platform")?.extract()?,
}),
})
}
/// Returns the name of the Python implementation for this environment.
#[getter]
pub fn py_implementation_name(&self) -> String {
self.implementation_name().to_string()
}
/// Returns the Python implementation version for this environment.
#[getter]
pub fn py_implementation_version(&self) -> StringVersion {
self.implementation_version().clone()
}
/// Returns the name of the operating system for this environment.
#[getter]
pub fn py_os_name(&self) -> String {
self.os_name().to_string()
}
/// Returns the name of the machine for this environment's platform.
#[getter]
pub fn py_platform_machine(&self) -> String {
self.platform_machine().to_string()
}
/// Returns the name of the Python implementation for this environment's
/// platform.
#[getter]
pub fn py_platform_python_implementation(&self) -> String {
self.platform_python_implementation().to_string()
}
/// Returns the release for this environment's platform.
#[getter]
pub fn py_platform_release(&self) -> String {
self.platform_release().to_string()
}
/// Returns the system for this environment's platform.
#[getter]
pub fn py_platform_system(&self) -> String {
self.platform_system().to_string()
}
/// Returns the version for this environment's platform.
#[getter]
pub fn py_platform_version(&self) -> String {
self.platform_version().to_string()
}
/// Returns the full version of Python for this environment.
#[getter]
pub fn py_python_full_version(&self) -> StringVersion {
self.python_full_version().clone()
}
/// Returns the version of Python for this environment.
#[getter]
pub fn py_python_version(&self) -> StringVersion {
self.python_version().clone()
}
/// Returns the name of the system platform for this environment.
#[getter]
pub fn py_sys_platform(&self) -> String {
self.sys_platform().to_string()
}
}
/// A builder for constructing a marker environment.
///
/// A value of this type can be fallibly converted to a full
/// [`MarkerEnvironment`] via [`MarkerEnvironment::try_from`]. This can fail when
/// the version strings given aren't valid.
///
/// The main utility of this type is for constructing dummy or test environment
/// values.
#[allow(missing_docs)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct MarkerEnvironmentBuilder<'a> {
pub implementation_name: &'a str,
pub implementation_version: &'a str,
pub os_name: &'a str,
pub platform_machine: &'a str,
pub platform_python_implementation: &'a str,
pub platform_release: &'a str,
pub platform_system: &'a str,
pub platform_version: &'a str,
pub python_full_version: &'a str,
pub python_version: &'a str,
pub sys_platform: &'a str,
}
impl<'a> TryFrom<MarkerEnvironmentBuilder<'a>> for MarkerEnvironment {
type Error = VersionParseError;
fn try_from(builder: MarkerEnvironmentBuilder<'a>) -> Result<Self, Self::Error> {
Ok(MarkerEnvironment {
inner: Arc::new(MarkerEnvironmentInner {
implementation_name: builder.implementation_name.to_string(),
implementation_version: builder.implementation_version.parse()?,
os_name: builder.os_name.to_string(),
platform_machine: builder.platform_machine.to_string(),
platform_python_implementation: builder.platform_python_implementation.to_string(),
platform_release: builder.platform_release.to_string(),
platform_system: builder.platform_system.to_string(),
platform_version: builder.platform_version.to_string(),
python_full_version: builder.python_full_version.parse()?,
python_version: builder.python_version.parse()?,
sys_platform: builder.sys_platform.to_string(),
}),
})
}
}
/// 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>`.
Version {
key: MarkerValueVersion,
specifier: VersionSpecifier,
},
/// An inverted version expression, e.g `<quoted PEP 440 version> <version op> <version key>`.
VersionInverted {
/// No star allowed here, `'3.*' == python_version` is not a valid PEP 440 comparison.
version: Version,
operator: pep440_rs::Operator,
key: MarkerValueVersion,
},
/// An string marker comparison, e.g. `sys_platform == '...'`.
String {
key: MarkerValueString,
operator: MarkerOperator,
value: String,
},
/// An inverted string marker comparison, e.g. `'...' == sys_platform`.
StringInverted {
value: String,
operator: MarkerOperator,
key: MarkerValueString,
},
/// `extra <extra op> '...'` or `'...' <extra op> extra`
Extra {
operator: ExtraOperator,
name: ExtraName,
},
/// An invalid or meaningless expression, such as '...' == '...'.
///
/// Invalid expressions always evaluate to `false`, and are warned for during parsing.
Arbitrary {
l_value: MarkerValue,
operator: MarkerOperator,
r_value: MarkerValue,
},
}
/// The operator for an extra expression, either '==' or '!='.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum ExtraOperator {
/// `==`
Equal,
/// `!=`
NotEqual,
}
impl ExtraOperator {
fn from_marker_operator(operator: MarkerOperator) -> Option<ExtraOperator> {
match operator {
MarkerOperator::Equal => Some(ExtraOperator::Equal),
MarkerOperator::NotEqual => Some(ExtraOperator::NotEqual),
_ => None,
}
}
}
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<Self, Pep508Error> {
let mut chars = Cursor::new(s);
let expression = 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)
}
/// Convert a <`marker_value`> <`marker_op`> <`marker_value`> expression into it's
/// typed equivalent.
fn new(
l_value: MarkerValue,
operator: MarkerOperator,
r_value: MarkerValue,
reporter: &mut impl Reporter,
) -> MarkerExpression {
match l_value {
// The only sound choice for this is `<version key> <version op> <quoted PEP 440 version>`
MarkerValue::MarkerEnvVersion(key) => {
let MarkerValue::QuotedString(value) = r_value else {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected double quoted PEP 440 version to compare with {key}, found {r_value},
will evaluate to false"
),
);
return MarkerExpression::arbitrary(
MarkerValue::MarkerEnvVersion(key),
operator,
r_value,
);
};
match MarkerExpression::version(key.clone(), operator, &value, reporter) {
Some(expr) => expr,
None => MarkerExpression::arbitrary(
MarkerValue::MarkerEnvVersion(key),
operator,
MarkerValue::QuotedString(value),
),
}
}
// The only sound choice for this is `<env key> <op> <string>`
MarkerValue::MarkerEnvString(key) => {
let value = match r_value {
MarkerValue::Extra
| MarkerValue::MarkerEnvVersion(_)
| MarkerValue::MarkerEnvString(_) => {
reporter.report(
MarkerWarningKind::MarkerMarkerComparison,
"Comparing two markers with each other doesn't make any sense,
will evaluate to false"
.to_string(),
);
return MarkerExpression::arbitrary(
MarkerValue::MarkerEnvString(key),
operator,
r_value,
);
}
MarkerValue::QuotedString(r_string) => r_string,
};
MarkerExpression::String {
key,
operator,
value,
}
}
// `extra == '...'`
MarkerValue::Extra => {
let value = match r_value {
MarkerValue::MarkerEnvVersion(_)
| MarkerValue::MarkerEnvString(_)
| MarkerValue::Extra => {
reporter.report(
MarkerWarningKind::ExtraInvalidComparison,
"Comparing extra with something other than a quoted string is wrong,
will evaluate to false"
.to_string(),
);
return MarkerExpression::arbitrary(l_value, operator, r_value);
}
MarkerValue::QuotedString(value) => value,
};
match MarkerExpression::extra(operator, &value, reporter) {
Some(expr) => expr,
None => MarkerExpression::arbitrary(
MarkerValue::Extra,
operator,
MarkerValue::QuotedString(value),
),
}
}
// This is either MarkerEnvVersion, MarkerEnvString or Extra inverted
MarkerValue::QuotedString(l_string) => {
match r_value {
// The only sound choice for this is `<quoted PEP 440 version> <version op>` <version key>
MarkerValue::MarkerEnvVersion(key) => {
match MarkerExpression::version_inverted(
&l_string,
operator,
key.clone(),
reporter,
) {
Some(expr) => expr,
None => MarkerExpression::arbitrary(
MarkerValue::QuotedString(l_string),
operator,
MarkerValue::MarkerEnvVersion(key),
),
}
}
// '...' == <env key>
MarkerValue::MarkerEnvString(key) => MarkerExpression::StringInverted {
key,
operator,
value: l_string,
},
// `'...' == extra`
MarkerValue::Extra => {
match MarkerExpression::extra(operator, &l_string, reporter) {
Some(expr) => expr,
None => MarkerExpression::arbitrary(
MarkerValue::QuotedString(l_string),
operator,
MarkerValue::Extra,
),
}
}
// `'...' == '...'`, doesn't make much sense
MarkerValue::QuotedString(_) => {
// Not even pypa/packaging 22.0 supports this
// https://github.com/pypa/packaging/issues/632
let expr = MarkerExpression::arbitrary(
MarkerValue::QuotedString(l_string),
operator,
r_value,
);
reporter.report(MarkerWarningKind::StringStringComparison, format!(
"Comparing two quoted strings with each other doesn't make sense: {expr},
will evaluate to false"
));
expr
}
}
}
}
}
/// Creates an instance of [`MarkerExpression::Arbitrary`] with the given values.
fn arbitrary(
l_value: MarkerValue,
operator: MarkerOperator,
r_value: MarkerValue,
) -> MarkerExpression {
MarkerExpression::Arbitrary {
l_value,
operator,
r_value,
}
}
/// Creates an instance of [`MarkerExpression::Version`] with the given values.
///
/// Reports a warning on failure, and returns `None`.
pub fn version(
key: MarkerValueVersion,
marker_operator: MarkerOperator,
value: &str,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
let pattern = match value.parse::<VersionPattern>() {
Ok(pattern) => pattern,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version to compare with {key}, found {value}, will evaluate to false: {err}"
),
);
return None;
}
};
let Some(operator) = marker_operator.to_pep440_operator() else {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version operator to compare {} with '{}',
found '{}', will evaluate to false",
key,
pattern.version(),
marker_operator
),
);
return None;
};
let specifier = match VersionSpecifier::from_pattern(operator, pattern) {
Ok(specifier) => specifier,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!("Invalid operator/version combination: {err}"),
);
return None;
}
};
Some(MarkerExpression::Version { key, specifier })
}
/// Creates an instance of [`MarkerExpression::VersionInverted`] with the given values.
///
/// Reports a warning on failure, and returns `None`.
fn version_inverted(
value: &str,
marker_operator: MarkerOperator,
key: MarkerValueVersion,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
let version = match value.parse::<Version>() {
Ok(version) => version,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version to compare with {key}, found {value}, will evaluate to false: {err}"
),
);
return None;
}
};
let Some(operator) = marker_operator.to_pep440_operator() else {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version operator to compare {key} with '{version}',
found '{marker_operator}', will evaluate to false"
),
);
return None;
};
Some(MarkerExpression::VersionInverted {
version,
operator,
key,
})
}
/// Creates an instance of [`MarkerExpression::Extra`] with the given values, falling back to
/// [`MarkerExpression::Arbitrary`] on failure.
fn extra(
operator: MarkerOperator,
value: &str,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
let name = match ExtraName::from_str(value) {
Ok(name) => name,
Err(err) => {
reporter.report(
MarkerWarningKind::ExtraInvalidComparison,
format!("Expected extra name, found '{value}', will evaluate to false: {err}"),
);
return None;
}
};
if let Some(operator) = ExtraOperator::from_marker_operator(operator) {
Some(MarkerExpression::Extra { operator, name })
} else {
reporter.report(
MarkerWarningKind::ExtraInvalidComparison,
"Comparing extra with something other than a quoted string is wrong,
will evaluate to false"
.to_string(),
);
None
}
}
/// Evaluate a <`marker_value`> <`marker_op`> <`marker_value`> expression
///
/// When `env` is `None`, all expressions that reference the environment
/// will evaluate as `true`.
fn evaluate(
&self,
env: Option<&MarkerEnvironment>,
extras: &[ExtraName],
reporter: &mut impl Reporter,
) -> bool {
match self {
MarkerExpression::Version { key, specifier } => env
.map(|env| specifier.contains(env.get_version(key)))
.unwrap_or(true),
MarkerExpression::VersionInverted {
key,
operator,
version,
} => env
.map(|env| {
let r_version = VersionPattern::verbatim(env.get_version(key).clone());
let specifier = match VersionSpecifier::from_pattern(*operator, r_version) {
Ok(specifier) => specifier,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!("Invalid operator/version combination: {err}"),
);
return false;
}
};
specifier.contains(version)
})
.unwrap_or(true),
MarkerExpression::String {
key,
operator,
value,
} => env
.map(|env| {
let l_string = env.get_string(key);
Self::compare_strings(l_string, *operator, value, reporter)
})
.unwrap_or(true),
MarkerExpression::StringInverted {
key,
operator,
value,
} => env
.map(|env| {
let r_string = env.get_string(key);
Self::compare_strings(value, *operator, r_string, reporter)
})
.unwrap_or(true),
MarkerExpression::Extra {
operator: ExtraOperator::Equal,
name,
} => extras.contains(name),
MarkerExpression::Extra {
operator: ExtraOperator::NotEqual,
name,
} => !extras.contains(name),
MarkerExpression::Arbitrary { .. } => false,
}
}
/// Evaluates only the extras and python version part of the markers. We use this during
/// dependency resolution when we want to have packages for all possible environments but
/// already know the extras and the possible python versions (from `requires-python`)
///
/// This considers only expression in the from `extra == '...'`, `'...' == extra`,
/// `python_version <pep PEP 440 operator> '...'` and
/// `'...' <pep PEP 440 operator> python_version`.
///
/// Note that unlike [`Self::evaluate`] this does not perform any checks for bogus expressions but
/// will simply return true.
///
/// ```rust
/// # use std::collections::HashSet;
/// # use std::str::FromStr;
/// # use pep508_rs::{MarkerTree, Pep508Error};
/// # use pep440_rs::Version;
/// # use uv_normalize::ExtraName;
///
/// # fn main() -> Result<(), Pep508Error> {
/// let marker_tree = MarkerTree::from_str(r#"("linux" in sys_platform) and extra == 'day'"#)?;
/// let versions: Vec<Version> = (8..12).map(|minor| Version::new([3, minor])).collect();
/// assert!(marker_tree.evaluate_extras_and_python_version(&[ExtraName::from_str("day").unwrap()].into(), &versions));
/// assert!(!marker_tree.evaluate_extras_and_python_version(&[ExtraName::from_str("night").unwrap()].into(), &versions));
///
/// let marker_tree = MarkerTree::from_str(r#"extra == 'day' and python_version < '3.11' and '3.10' <= python_version"#)?;
/// assert!(!marker_tree.evaluate_extras_and_python_version(&[ExtraName::from_str("day").unwrap()].into(), &vec![Version::new([3, 9])]));
/// assert!(marker_tree.evaluate_extras_and_python_version(&[ExtraName::from_str("day").unwrap()].into(), &vec![Version::new([3, 10])]));
/// assert!(!marker_tree.evaluate_extras_and_python_version(&[ExtraName::from_str("day").unwrap()].into(), &vec![Version::new([3, 11])]));
/// # Ok(())
/// # }
/// ```
fn evaluate_extras_and_python_version(
&self,
extras: &HashSet<ExtraName>,
python_versions: &[Version],
) -> bool {
match self {
MarkerExpression::Version {
key: MarkerValueVersion::PythonVersion,
specifier,
} => python_versions
.iter()
.any(|l_version| specifier.contains(l_version)),
MarkerExpression::VersionInverted {
key: MarkerValueVersion::PythonVersion,
operator,
version,
} => {
python_versions.iter().any(|r_version| {
// operator and right hand side make the specifier and in this case the
// right hand is `python_version` so changes every iteration
let Ok(specifier) = VersionSpecifier::from_pattern(
*operator,
VersionPattern::verbatim(r_version.clone()),
) else {
return true;
};
specifier.contains(version)
})
}
MarkerExpression::Extra {
operator: ExtraOperator::Equal,
name,
} => extras.contains(name),
MarkerExpression::Extra {
operator: ExtraOperator::NotEqual,
name,
} => !extras.contains(name),
_ => true,
}
}
/// Compare strings by PEP 508 logic, with warnings
fn compare_strings(
l_string: &str,
operator: MarkerOperator,
r_string: &str,
reporter: &mut impl Reporter,
) -> bool {
match operator {
MarkerOperator::Equal => l_string == r_string,
MarkerOperator::NotEqual => l_string != r_string,
MarkerOperator::GreaterThan => {
reporter.report(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {l_string} and {r_string} lexicographically"),
);
l_string > r_string
}
MarkerOperator::GreaterEqual => {
reporter.report(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {l_string} and {r_string} lexicographically"),
);
l_string >= r_string
}
MarkerOperator::LessThan => {
reporter.report(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {l_string} and {r_string} lexicographically"),
);
l_string < r_string
}
MarkerOperator::LessEqual => {
reporter.report(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {l_string} and {r_string} lexicographically"),
);
l_string <= r_string
}
MarkerOperator::TildeEqual => {
reporter.report(
MarkerWarningKind::LexicographicComparison,
format!("Can't compare {l_string} and {r_string} with `~=`"),
);
false
}
MarkerOperator::In => r_string.contains(l_string),
MarkerOperator::NotIn => !r_string.contains(l_string),
}
}
}
impl FromStr for MarkerExpression {
type Err = Pep508Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 } => {
write!(
f,
"{key} {} '{}'",
specifier.operator(),
specifier.version()
)
}
MarkerExpression::VersionInverted {
version,
operator,
key,
} => {
write!(f, "'{version}' {operator} {key}")
}
MarkerExpression::String {
key,
operator,
value,
} => {
write!(f, "{key} {operator} '{value}'")
}
MarkerExpression::StringInverted {
value,
operator,
key,
} => {
write!(f, "'{value}' {operator} {key}")
}
MarkerExpression::Extra { operator, name } => {
write!(f, "extra {operator} '{name}'")
}
MarkerExpression::Arbitrary {
l_value,
operator,
r_value,
} => {
write!(f, "{l_value} {operator} {r_value}")
}
}
}
}
/// Represents one of the nested marker expressions with and/or/parentheses
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum MarkerTree {
/// A simple expression such as `python_version > "3.8"`
Expression(MarkerExpression),
/// An and between nested expressions, such as
/// `python_version > "3.8" and implementation_name == 'cpython'`
And(Vec<MarkerTree>),
/// An or between nested expressions, such as
/// `python_version > "3.8" or implementation_name == 'cpython'`
Or(Vec<MarkerTree>),
}
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 Serialize for MarkerTree {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl FromStr for MarkerTree {
type Err = Pep508Error;
fn from_str(markers: &str) -> Result<Self, Self::Err> {
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_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_markers(markers, reporter)
}
/// Whether the marker is `MarkerTree::And(Vec::new())`.
pub fn is_universal(&self) -> bool {
self == &MarkerTree::And(Vec::new())
}
/// Does this marker apply in the given environment?
pub fn evaluate(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
self.evaluate_optional_environment(Some(env), extras)
}
/// 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 {
self.report_deprecated_options(&mut TracingReporter);
match self {
Self::Expression(expression) => expression.evaluate(env, extras, &mut TracingReporter),
Self::And(expressions) => expressions
.iter()
.all(|x| x.evaluate_reporter_impl(env, extras, &mut TracingReporter)),
Self::Or(expressions) => expressions
.iter()
.any(|x| x.evaluate_reporter_impl(env, extras, &mut TracingReporter)),
}
}
/// 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'`.
pub fn simplify_extras(self, extras: &[ExtraName]) -> Option<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'`.
pub fn simplify_extras_with(self, is_extra: impl Fn(&ExtraName) -> bool) -> Option<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,
) -> Option<MarkerTree> {
/// Returns `true` if the given expression is always `true` given the set of extras.
fn is_true(expression: &MarkerExpression, is_extra: impl Fn(&ExtraName) -> bool) -> bool {
match expression {
MarkerExpression::Extra {
operator: ExtraOperator::Equal,
name,
} => is_extra(name),
MarkerExpression::Extra {
operator: ExtraOperator::NotEqual,
name,
} => !is_extra(name),
_ => false,
}
}
match self {
Self::Expression(expression) => {
// If the expression is true, we can remove the marker entirely.
if is_true(&expression, is_extra) {
None
} else {
// If not, return the original marker.
Some(Self::Expression(expression))
}
}
Self::And(expressions) => {
// Remove any expressions that are _true_ due to the presence of an extra.
let simplified = expressions
.into_iter()
.filter_map(|marker| marker.simplify_extras_with_impl(is_extra))
.collect::<Vec<_>>();
// If there are no expressions left, return None.
if simplified.is_empty() {
None
} else if simplified.len() == 1 {
// If there is only one expression left, return the remaining expression.
simplified.into_iter().next()
} else {
// If there are still expressions left, return the simplified marker.
Some(Self::And(simplified))
}
}
Self::Or(expressions) => {
let num_expressions = expressions.len();
// Remove any expressions that are _true_ due to the presence of an extra.
let simplified = expressions
.into_iter()
.filter_map(|marker| marker.simplify_extras_with_impl(is_extra))
.collect::<Vec<_>>();
// If _any_ of the expressions are true (i.e., if any of the markers were filtered
// out in the above filter step), the entire marker is true.
if simplified.len() < num_expressions {
None
} else if simplified.len() == 1 {
// If there is only one expression left, return the remaining expression.
simplified.into_iter().next()
} else {
// If there are still expressions left, return the simplified marker.
Some(Self::Or(simplified))
}
}
}
}
/// 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.report_deprecated_options(reporter);
self.evaluate_reporter_impl(Some(env), extras, reporter)
}
fn evaluate_reporter_impl(
&self,
env: Option<&MarkerEnvironment>,
extras: &[ExtraName],
reporter: &mut impl Reporter,
) -> bool {
match self {
Self::Expression(expression) => expression.evaluate(env, extras, reporter),
Self::And(expressions) => expressions
.iter()
.all(|x| x.evaluate_reporter_impl(env, extras, reporter)),
Self::Or(expressions) => expressions
.iter()
.any(|x| x.evaluate_reporter_impl(env, extras, reporter)),
}
}
/// Checks if the requirement should be activated with the given set of active extras and a set
/// of possible python versions (from `requires-python`) without evaluating the remaining
/// environment markers, i.e. if there is potentially an environment that could activate this
/// requirement.
///
/// Note that unlike [`Self::evaluate`] this does not perform any checks for bogus expressions but
/// will simply return true. As caller you should separately perform a check with an environment
/// and forward all warnings.
pub fn evaluate_extras_and_python_version(
&self,
extras: &HashSet<ExtraName>,
python_versions: &[Version],
) -> bool {
match self {
Self::Expression(expression) => {
expression.evaluate_extras_and_python_version(extras, python_versions)
}
Self::And(expressions) => expressions
.iter()
.all(|x| x.evaluate_extras_and_python_version(extras, python_versions)),
Self::Or(expressions) => expressions
.iter()
.any(|x| x.evaluate_extras_and_python_version(extras, python_versions)),
}
}
/// Same as [`Self::evaluate`], but instead of using logging to warn, you get a Vec with all
/// warnings collected
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(Some(env), extras, &mut reporter);
(result, warnings)
}
/// Report the deprecated marker from <https://peps.python.org/pep-0345/#environment-markers>
fn report_deprecated_options(&self, reporter: &mut impl Reporter) {
match self {
Self::Expression(expression) => {
let MarkerExpression::String { key, .. } = expression else {
return;
};
match 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(),
);
}
_ => {}
}
}
Self::And(expressions) => {
for expression in expressions {
expression.report_deprecated_options(reporter);
}
}
Self::Or(expressions) => {
for expression in expressions {
expression.report_deprecated_options(reporter);
}
}
}
}
/// Combine this marker tree with the one given via a conjunction.
///
/// This does some shallow flattening. That is, if `self` is a conjunction
/// already, then `tree` is added to it instead of creating a new
/// conjunction.
pub fn and(&mut self, tree: MarkerTree) {
match *self {
MarkerTree::Expression(_) | MarkerTree::Or(_) => {
let this = std::mem::replace(self, MarkerTree::And(vec![]));
*self = MarkerTree::And(vec![this]);
}
MarkerTree::And(_) => {}
}
if let MarkerTree::And(ref mut exprs) = *self {
if let MarkerTree::And(tree) = tree {
exprs.extend(tree);
} else {
exprs.push(tree);
}
}
}
/// Combine this marker tree with the one given via a disjunction.
///
/// This does some shallow flattening. That is, if `self` is a disjunction
/// already, then `tree` is added to it instead of creating a new
/// disjunction.
pub fn or(&mut self, tree: MarkerTree) {
match *self {
MarkerTree::Expression(_) | MarkerTree::And(_) => {
let this = std::mem::replace(self, MarkerTree::And(vec![]));
*self = MarkerTree::Or(vec![this]);
}
MarkerTree::Or(_) => {}
}
if let MarkerTree::Or(ref mut exprs) = *self {
if let MarkerTree::Or(tree) = tree {
exprs.extend(tree);
} else {
exprs.push(tree);
}
}
}
}
impl Display for MarkerTree {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let format_inner = |expression: &Self| {
if matches!(expression, Self::Expression(_)) {
format!("{expression}")
} else {
format!("({expression})")
}
};
match self {
Self::Expression(expression) => write!(f, "{expression}"),
Self::And(and_list) => f.write_str(
&and_list
.iter()
.map(format_inner)
.collect::<Vec<String>>()
.join(" and "),
),
Self::Or(or_list) => f.write_str(
&or_list
.iter()
.map(format_inner)
.collect::<Vec<String>>()
.join(" or "),
),
}
}
}
/// ```text
/// version_cmp = wsp* <'<=' | '<' | '!=' | '==' | '>=' | '>' | '~=' | '==='>
/// marker_op = version_cmp | (wsp* 'in') | (wsp* 'not' wsp+ 'in')
/// ```
/// The `wsp*` has already been consumed by the caller.
fn parse_marker_operator<T: Pep508Url>(
cursor: &mut Cursor,
) -> Result<MarkerOperator, Pep508Error<T>> {
let (start, len) = if cursor.peek_char().is_some_and(char::is_alphabetic) {
// "in" or "not"
cursor.take_while(|char| !char.is_whitespace() && char != '\'' && char != '"')
} else {
// A mathematical operator
cursor.take_while(|char| matches!(char, '<' | '=' | '>' | '~' | '!'))
};
let operator = cursor.slice(start, len);
if operator == "not" {
// 'not' wsp+ 'in'
match cursor.next() {
None => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(
"Expected whitespace after 'not', found end of input".to_string(),
),
start: cursor.pos(),
len: 1,
input: cursor.to_string(),
});
}
Some((_, whitespace)) if whitespace.is_whitespace() => {}
Some((pos, other)) => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected whitespace after 'not', found '{other}'"
)),
start: pos,
len: other.len_utf8(),
input: cursor.to_string(),
});
}
};
cursor.eat_whitespace();
cursor.next_expect_char('i', cursor.pos())?;
cursor.next_expect_char('n', cursor.pos())?;
return Ok(MarkerOperator::NotIn);
}
MarkerOperator::from_str(operator).map_err(|_| Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected a valid marker operator (such as '>=' or 'not in'), found '{operator}'"
)),
start,
len,
input: cursor.to_string(),
})
}
/// Either a single or double quoted string or one of '`python_version`', '`python_full_version`',
/// '`os_name`', '`sys_platform`', '`platform_release`', '`platform_system`', '`platform_version`',
/// '`platform_machine`', '`platform_python_implementation`', '`implementation_name`',
/// '`implementation_version`', 'extra'
fn parse_marker_value<T: Pep508Url>(cursor: &mut Cursor) -> Result<MarkerValue, Pep508Error<T>> {
// > User supplied constants are always encoded as strings with either ' or " quote marks. Note
// > that backslash escapes are not defined, but existing implementations do support them. They
// > are not included in this specification because they add complexity and there is no observable
// > need for them today. Similarly we do not define non-ASCII character support: all the runtime
// > variables we are referencing are expected to be ASCII-only.
match cursor.peek() {
None => Err(Pep508Error {
message: Pep508ErrorSource::String(
"Expected marker value, found end of dependency specification".to_string(),
),
start: cursor.pos(),
len: 1,
input: cursor.to_string(),
}),
// It can be a string ...
Some((start_pos, quotation_mark @ ('"' | '\''))) => {
cursor.next();
let (start, len) = cursor.take_while(|c| c != quotation_mark);
let value = cursor.slice(start, len).to_string();
cursor.next_expect_char(quotation_mark, start_pos)?;
Ok(MarkerValue::string_value(value))
}
// ... or it can be a keyword
Some(_) => {
let (start, len) = cursor.take_while(|char| {
!char.is_whitespace() && !['>', '=', '<', '!', '~', ')'].contains(&char)
});
let key = cursor.slice(start, len);
MarkerValue::from_str(key).map_err(|_| Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected a valid marker name, found '{key}'"
)),
start,
len,
input: cursor.to_string(),
})
}
}
}
/// ```text
/// marker_var:l marker_op:o marker_var:r
/// ```
fn parse_marker_key_op_value<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<MarkerExpression, Pep508Error<T>> {
cursor.eat_whitespace();
let lvalue = parse_marker_value(cursor)?;
cursor.eat_whitespace();
// "not in" and "in" must be preceded by whitespace. We must already have matched a whitespace
// when we're here because other `parse_marker_key` would have pulled the characters in and
// errored
let operator = parse_marker_operator(cursor)?;
cursor.eat_whitespace();
let rvalue = parse_marker_value(cursor)?;
Ok(MarkerExpression::new(lvalue, operator, rvalue, reporter))
}
/// ```text
/// marker_expr = marker_var:l marker_op:o marker_var:r -> (o, l, r)
/// | wsp* '(' marker:m wsp* ')' -> m
/// ```
fn parse_marker_expr<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<MarkerTree, Pep508Error<T>> {
cursor.eat_whitespace();
if let Some(start_pos) = cursor.eat_char('(') {
let marker = parse_marker_or(cursor, reporter)?;
cursor.next_expect_char(')', start_pos)?;
Ok(marker)
} else {
Ok(MarkerTree::Expression(parse_marker_key_op_value(
cursor, reporter,
)?))
}
}
/// ```text
/// marker_and = marker_expr:l wsp* 'and' marker_expr:r -> ('and', l, r)
/// | marker_expr:m -> m
/// ```
fn parse_marker_and<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<MarkerTree, Pep508Error<T>> {
parse_marker_op(cursor, "and", MarkerTree::And, parse_marker_expr, reporter)
}
/// ```text
/// marker_or = marker_and:l wsp* 'or' marker_and:r -> ('or', l, r)
/// | marker_and:m -> m
/// ```
fn parse_marker_or<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<MarkerTree, Pep508Error<T>> {
parse_marker_op(cursor, "or", MarkerTree::Or, parse_marker_and, reporter)
}
/// Parses both `marker_and` and `marker_or`
fn parse_marker_op<T: Pep508Url, R: Reporter>(
cursor: &mut Cursor,
op: &str,
op_constructor: fn(Vec<MarkerTree>) -> MarkerTree,
parse_inner: fn(&mut Cursor, &mut R) -> Result<MarkerTree, Pep508Error<T>>,
reporter: &mut R,
) -> Result<MarkerTree, Pep508Error<T>> {
// marker_and or marker_expr
let first_element = parse_inner(cursor, reporter)?;
// wsp*
cursor.eat_whitespace();
// Check if we're done here instead of invoking the whole vec allocating loop
if matches!(cursor.peek_char(), None | Some(')')) {
return Ok(first_element);
}
let mut expressions = Vec::with_capacity(1);
expressions.push(first_element);
loop {
// wsp*
cursor.eat_whitespace();
// ('or' marker_and) or ('and' marker_or)
let (start, len) = cursor.peek_while(|c| !c.is_whitespace());
match cursor.slice(start, len) {
value if value == op => {
cursor.take_while(|c| !c.is_whitespace());
let expression = parse_inner(cursor, reporter)?;
expressions.push(expression);
}
_ => {
// Build minimal trees
return if expressions.len() == 1 {
Ok(expressions.remove(0))
} else {
Ok(op_constructor(expressions))
};
}
}
}
}
/// ```text
/// marker = marker_or^
/// ```
pub(crate) fn parse_markers_cursor<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<MarkerTree, Pep508Error<T>> {
let marker = parse_marker_or(cursor, reporter)?;
cursor.eat_whitespace();
if let Some((pos, unexpected)) = cursor.next() {
// If we're here, both parse_marker_or and parse_marker_and returned because the next
// character was neither "and" nor "or"
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Unexpected character '{unexpected}', expected 'and', 'or' or end of input"
)),
start: pos,
len: cursor.remaining(),
input: cursor.to_string(),
});
};
Ok(marker)
}
/// Parses markers such as `python_version < '3.8'` or
/// `python_version == "3.10" and (sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython'))`
fn parse_markers<T: Pep508Url>(
markers: &str,
reporter: &mut impl Reporter,
) -> Result<MarkerTree, Pep508Error<T>> {
let mut chars = Cursor::new(markers);
parse_markers_cursor(&mut chars, reporter)
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use insta::assert_snapshot;
use pep440_rs::VersionSpecifier;
use uv_normalize::ExtraName;
use crate::marker::{ExtraOperator, MarkerEnvironment, MarkerEnvironmentBuilder};
use crate::{
MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString, MarkerValueVersion,
};
fn parse_err(input: &str) -> String {
MarkerTree::from_str(input).unwrap_err().to_string()
}
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!(
MarkerTree::from_str(a).unwrap(),
MarkerTree::from_str(b).unwrap(),
"{a} {b}"
);
}
}
#[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]
#[cfg(feature = "tracing")]
fn warnings() {
let env37 = env37();
testing_logger::setup();
let compare_keys = MarkerTree::from_str("platform_version == sys_platform").unwrap();
compare_keys.evaluate(&env37, &[]);
testing_logger::validate(|captured_logs| {
assert_eq!(
captured_logs[0].body,
"Comparing two markers with each other doesn't make any sense, will evaluate to false"
);
assert_eq!(captured_logs[0].level, log::Level::Warn);
assert_eq!(captured_logs.len(), 1);
});
let non_pep440 = MarkerTree::from_str("python_version >= '3.9.'").unwrap();
non_pep440.evaluate(&env37, &[]);
testing_logger::validate(|captured_logs| {
assert_eq!(
captured_logs[0].body,
"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"
);
assert_eq!(captured_logs[0].level, log::Level::Warn);
assert_eq!(captured_logs.len(), 1);
});
let string_string = MarkerTree::from_str("'b' >= 'a'").unwrap();
string_string.evaluate(&env37, &[]);
testing_logger::validate(|captured_logs| {
assert_eq!(
captured_logs[0].body,
"Comparing two quoted strings with each other doesn't make sense: 'b' >= 'a', will evaluate to false"
);
assert_eq!(captured_logs[0].level, log::Level::Warn);
assert_eq!(captured_logs.len(), 1);
});
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, &[]);
testing_logger::validate(|captured_logs| {
let messages: Vec<_> = captured_logs
.iter()
.map(|message| {
assert_eq!(message.level, log::Level::Warn);
&message.body
})
.collect();
let expected = [
"os.name is deprecated in favor of os_name",
"platform.machine is deprecated in favor of platform_machine",
"platform.python_implementation is deprecated in favor of platform_python_implementation",
"platform.version is deprecated in favor of platform_version",
"sys.platform is deprecated in favor of sys_platform"
];
assert_eq!(messages, &expected);
});
}
#[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);
}
#[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(),
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(),
MarkerTree::And(vec![
MarkerTree::Expression(MarkerExpression::StringInverted {
value: "nt".to_string(),
operator: MarkerOperator::In,
key: MarkerValueString::OsName,
}),
MarkerTree::Expression(MarkerExpression::VersionInverted {
key: MarkerValueVersion::PythonVersion,
operator: pep440_rs::Operator::GreaterThanEqual,
version: "3.7".parse().unwrap(),
}),
MarkerTree::Expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::from_pattern(
pep440_rs::Operator::GreaterThanEqual,
"3.7".parse().unwrap()
)
.unwrap()
}),
])
);
}
#[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()]);
assert_eq!(
simplified,
Some(MarkerTree::Expression(MarkerExpression::String {
key: MarkerValueString::OsName,
operator: MarkerOperator::Equal,
value: "nt".to_string(),
}))
);
// 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, None);
// 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, None);
// 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()]);
assert_eq!(
simplified,
Some(MarkerTree::Expression(MarkerExpression::Extra {
operator: ExtraOperator::Equal,
name: ExtraName::from_str("test").unwrap(),
}))
);
// 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.simplify_extras(&[ExtraName::from_str("dev").unwrap()]);
assert_eq!(
simplified,
Some(MarkerTree::And(vec![
MarkerTree::Expression(MarkerExpression::String {
key: MarkerValueString::OsName,
operator: MarkerOperator::Equal,
value: "nt".to_string(),
}),
MarkerTree::Expression(MarkerExpression::Extra {
operator: ExtraOperator::Equal,
name: ExtraName::from_str("test").unwrap(),
}),
]))
);
// 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()]);
assert_eq!(
simplified,
Some(MarkerTree::Expression(MarkerExpression::String {
key: MarkerValueString::OsName,
operator: MarkerOperator::Equal,
value: "nt".to_string(),
}))
);
// 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()]);
assert_eq!(
simplified,
Some(MarkerTree::Or(vec![
MarkerTree::Expression(MarkerExpression::String {
key: MarkerValueString::OsName,
operator: MarkerOperator::Equal,
value: "nt".to_string(),
}),
MarkerTree::Expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::from_pattern(
pep440_rs::Operator::Equal,
"3.7".parse().unwrap()
)
.unwrap(),
}),
]))
);
}
}