Normalize extras when evaluating PEP 508 markers (#915)

## Summary

We always normalize extra names in our requirements (e.g., `cuda12_pip`
to `cuda12-pip`), but we weren't normalizing within PEP 508 markers,
which meant we ended up comparing `cuda12-pip` (normalized) against
`cuda12_pip` (unnormalized).

Closes https://github.com/astral-sh/puffin/issues/911.
This commit is contained in:
Charlie Marsh 2024-01-14 12:16:54 -05:00 committed by GitHub
parent a99e5e00f2
commit 0374000ec0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 52 deletions

View file

@ -41,6 +41,8 @@ pub use marker::{
MarkerValueString, MarkerValueVersion, MarkerWarningKind, StringVersion, MarkerValueString, MarkerValueVersion, MarkerWarningKind, StringVersion,
}; };
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
#[cfg(feature = "pyo3")]
use puffin_normalize::InvalidNameError;
use puffin_normalize::{ExtraName, PackageName}; use puffin_normalize::{ExtraName, PackageName};
pub use verbatim_url::VerbatimUrl; pub use verbatim_url::VerbatimUrl;
@ -192,6 +194,8 @@ impl Serialize for Requirement {
} }
} }
type MarkerWarning = (MarkerWarningKind, String, String);
#[cfg(feature = "pyo3")] #[cfg(feature = "pyo3")]
#[pymethods] #[pymethods]
impl Requirement { impl Requirement {
@ -266,11 +270,18 @@ impl Requirement {
/// Returns whether the markers apply for the given environment /// Returns whether the markers apply for the given environment
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
#[pyo3(name = "evaluate_markers")] #[pyo3(name = "evaluate_markers")]
pub fn py_evaluate_markers(&self, env: &MarkerEnvironment, extras: Vec<String>) -> bool { pub fn py_evaluate_markers(
self.evaluate_markers( &self,
env, env: &MarkerEnvironment,
&extras.iter().map(String::as_str).collect::<Vec<&str>>(), extras: Vec<String>,
) ) -> PyResult<bool> {
let extras = extras
.into_iter()
.map(|extra| ExtraName::from_str(&extra))
.collect::<Result<Vec<_>, InvalidNameError>>()
.map_err(|err| PyPep508Error::new_err(err.to_string()))?;
Ok(self.evaluate_markers(env, &extras))
} }
/// Returns whether the requirement would be satisfied, independent of environment markers, i.e. /// Returns whether the requirement would be satisfied, independent of environment markers, i.e.
@ -285,14 +296,19 @@ impl Requirement {
&self, &self,
extras: HashSet<String>, extras: HashSet<String>,
python_versions: Vec<PyVersion>, python_versions: Vec<PyVersion>,
) -> bool { ) -> PyResult<bool> {
self.evaluate_extras_and_python_version( let extras = extras
&extras, .into_iter()
&python_versions .map(|extra| ExtraName::from_str(&extra))
.collect::<Result<HashSet<_>, InvalidNameError>>()
.map_err(|err| PyPep508Error::new_err(err.to_string()))?;
let python_versions = python_versions
.into_iter() .into_iter()
.map(|py_version| py_version.0) .map(|py_version| py_version.0)
.collect::<Vec<_>>(), .collect::<Vec<_>>();
)
Ok(self.evaluate_extras_and_python_version(&extras, &python_versions))
} }
/// Returns whether the markers apply for the given environment /// Returns whether the markers apply for the given environment
@ -302,8 +318,14 @@ impl Requirement {
&self, &self,
env: &MarkerEnvironment, env: &MarkerEnvironment,
extras: Vec<String>, extras: Vec<String>,
) -> (bool, Vec<(MarkerWarningKind, String, String)>) { ) -> PyResult<(bool, Vec<MarkerWarning>)> {
self.evaluate_markers_and_report(env, &extras) let extras = extras
.into_iter()
.map(|extra| ExtraName::from_str(&extra))
.collect::<Result<Vec<_>, InvalidNameError>>()
.map_err(|err| PyPep508Error::new_err(err.to_string()))?;
Ok(self.evaluate_markers_and_report(env, &extras))
} }
} }
@ -326,7 +348,7 @@ impl Requirement {
} }
/// Returns whether the markers apply for the given environment /// Returns whether the markers apply for the given environment
pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[&str]) -> bool { pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
if let Some(marker) = &self.marker { if let Some(marker) = &self.marker {
marker.evaluate(env, extras) marker.evaluate(env, extras)
} else { } else {
@ -342,7 +364,7 @@ impl Requirement {
/// with an environment and forward all warnings. /// with an environment and forward all warnings.
pub fn evaluate_extras_and_python_version( pub fn evaluate_extras_and_python_version(
&self, &self,
extras: &HashSet<String>, extras: &HashSet<ExtraName>,
python_versions: &[Version], python_versions: &[Version],
) -> bool { ) -> bool {
if let Some(marker) = &self.marker { if let Some(marker) = &self.marker {
@ -356,16 +378,10 @@ impl Requirement {
pub fn evaluate_markers_and_report( pub fn evaluate_markers_and_report(
&self, &self,
env: &MarkerEnvironment, env: &MarkerEnvironment,
extras: &[String], extras: &[ExtraName],
) -> (bool, Vec<(MarkerWarningKind, String, String)>) { ) -> (bool, Vec<MarkerWarning>) {
if let Some(marker) = &self.marker { if let Some(marker) = &self.marker {
marker.evaluate_collect_warnings( marker.evaluate_collect_warnings(env, extras)
env,
&extras
.iter()
.map(std::string::String::as_str)
.collect::<Vec<&str>>(),
)
} else { } else {
(true, Vec::new()) (true, Vec::new())
} }

View file

@ -11,6 +11,7 @@
use crate::{Cursor, Pep508Error, Pep508ErrorSource}; use crate::{Cursor, Pep508Error, Pep508ErrorSource};
use pep440_rs::{Version, VersionPattern, VersionSpecifier}; use pep440_rs::{Version, VersionPattern, VersionSpecifier};
use puffin_normalize::ExtraName;
#[cfg(feature = "pyo3")] #[cfg(feature = "pyo3")]
use pyo3::{ use pyo3::{
basic::CompareOp, exceptions::PyValueError, pyclass, pymethods, PyAny, PyResult, Python, basic::CompareOp, exceptions::PyValueError, pyclass, pymethods, PyAny, PyResult, Python,
@ -552,7 +553,7 @@ impl MarkerExpression {
fn evaluate( fn evaluate(
&self, &self,
env: &MarkerEnvironment, env: &MarkerEnvironment,
extras: &[&str], extras: &[ExtraName],
reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression), reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression),
) -> bool { ) -> bool {
match &self.l_value { match &self.l_value {
@ -630,7 +631,15 @@ impl MarkerExpression {
} }
MarkerValue::QuotedString(r_value_string) => r_value_string, MarkerValue::QuotedString(r_value_string) => r_value_string,
}; };
self.marker_compare(r_value_string, extras, reporter) match ExtraName::from_str(r_value_string) {
Ok(r_extra) => extras.contains(&r_extra),
Err(err) => {
reporter(MarkerWarningKind::ExtraInvalidComparison, format!(
"Expected extra name, found '{r_value_string}', evaluating to false: {err}"
), self);
false
}
}
} }
// This is either MarkerEnvVersion, MarkerEnvString or Extra inverted // This is either MarkerEnvVersion, MarkerEnvString or Extra inverted
MarkerValue::QuotedString(l_string) => { MarkerValue::QuotedString(l_string) => {
@ -683,7 +692,15 @@ impl MarkerExpression {
self.compare_strings(l_string, r_string, reporter) self.compare_strings(l_string, r_string, reporter)
} }
// `'...' == extra` // `'...' == extra`
MarkerValue::Extra => self.marker_compare(l_string, extras, reporter), MarkerValue::Extra => match ExtraName::from_str(l_string) {
Ok(l_extra) => self.marker_compare(&l_extra, extras, reporter),
Err(err) => {
reporter(MarkerWarningKind::ExtraInvalidComparison, format!(
"Expected extra name, found '{l_string}', evaluating to false: {err}"
), self);
false
}
},
// `'...' == '...'`, doesn't make much sense // `'...' == '...'`, doesn't make much sense
MarkerValue::QuotedString(_) => { MarkerValue::QuotedString(_) => {
// Not even pypa/packaging 22.0 supports this // Not even pypa/packaging 22.0 supports this
@ -730,25 +747,25 @@ impl MarkerExpression {
/// ``` /// ```
fn evaluate_extras_and_python_version( fn evaluate_extras_and_python_version(
&self, &self,
extras: &HashSet<String>, extras: &HashSet<ExtraName>,
python_versions: &[Version], python_versions: &[Version],
) -> bool { ) -> bool {
match (&self.l_value, &self.operator, &self.r_value) { match (&self.l_value, &self.operator, &self.r_value) {
// `extra == '...'` // `extra == '...'`
(MarkerValue::Extra, MarkerOperator::Equal, MarkerValue::QuotedString(r_string)) => { (MarkerValue::Extra, MarkerOperator::Equal, MarkerValue::QuotedString(r_string)) => {
extras.contains(r_string) ExtraName::from_str(r_string).is_ok_and(|r_extra| extras.contains(&r_extra))
} }
// `'...' == extra` // `'...' == extra`
(MarkerValue::QuotedString(l_string), MarkerOperator::Equal, MarkerValue::Extra) => { (MarkerValue::QuotedString(l_string), MarkerOperator::Equal, MarkerValue::Extra) => {
extras.contains(l_string) ExtraName::from_str(l_string).is_ok_and(|l_extra| extras.contains(&l_extra))
} }
// `extra != '...'` // `extra != '...'`
(MarkerValue::Extra, MarkerOperator::NotEqual, MarkerValue::QuotedString(r_string)) => { (MarkerValue::Extra, MarkerOperator::NotEqual, MarkerValue::QuotedString(r_string)) => {
!extras.contains(r_string) ExtraName::from_str(r_string).is_ok_and(|r_extra| !extras.contains(&r_extra))
} }
// `'...' != extra` // `'...' != extra`
(MarkerValue::QuotedString(l_string), MarkerOperator::NotEqual, MarkerValue::Extra) => { (MarkerValue::QuotedString(l_string), MarkerOperator::NotEqual, MarkerValue::Extra) => {
!extras.contains(l_string) ExtraName::from_str(l_string).is_ok_and(|l_extra| !extras.contains(&l_extra))
} }
( (
MarkerValue::MarkerEnvVersion(MarkerValueVersion::PythonVersion), MarkerValue::MarkerEnvVersion(MarkerValueVersion::PythonVersion),
@ -860,14 +877,13 @@ impl MarkerExpression {
// The `marker <op> '...'` comparison // The `marker <op> '...'` comparison
fn marker_compare( fn marker_compare(
&self, &self,
value: &str, value: &ExtraName,
extras: &[&str], extras: &[ExtraName],
reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression), reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression),
) -> bool { ) -> bool {
match self.operator { match self.operator {
// TODO: normalize extras MarkerOperator::Equal => extras.contains(value),
MarkerOperator::Equal => extras.contains(&value), MarkerOperator::NotEqual => !extras.contains(value),
MarkerOperator::NotEqual => !extras.contains(&value),
_ => { _ => {
reporter(MarkerWarningKind::ExtraInvalidComparison, "Comparing extra with something other than equal (`==`) or unequal (`!=`) is wrong, evaluating to false".to_string(), self); reporter(MarkerWarningKind::ExtraInvalidComparison, "Comparing extra with something other than equal (`==`) or unequal (`!=`) is wrong, evaluating to false".to_string(), self);
false false
@ -926,7 +942,7 @@ impl FromStr for MarkerTree {
impl MarkerTree { impl MarkerTree {
/// Does this marker apply in the given environment? /// Does this marker apply in the given environment?
pub fn evaluate(&self, env: &MarkerEnvironment, extras: &[&str]) -> bool { pub fn evaluate(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
let mut reporter = |_kind, message, _marker_expression: &MarkerExpression| { let mut reporter = |_kind, message, _marker_expression: &MarkerExpression| {
warn!("{}", message); warn!("{}", message);
}; };
@ -947,7 +963,7 @@ impl MarkerTree {
pub fn evaluate_reporter( pub fn evaluate_reporter(
&self, &self,
env: &MarkerEnvironment, env: &MarkerEnvironment,
extras: &[&str], extras: &[ExtraName],
reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression), reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression),
) -> bool { ) -> bool {
self.report_deprecated_options(reporter); self.report_deprecated_options(reporter);
@ -957,7 +973,7 @@ impl MarkerTree {
fn evaluate_reporter_impl( fn evaluate_reporter_impl(
&self, &self,
env: &MarkerEnvironment, env: &MarkerEnvironment,
extras: &[&str], extras: &[ExtraName],
reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression), reporter: &mut impl FnMut(MarkerWarningKind, String, &MarkerExpression),
) -> bool { ) -> bool {
match self { match self {
@ -981,7 +997,7 @@ impl MarkerTree {
/// and forward all warnings. /// and forward all warnings.
pub fn evaluate_extras_and_python_version( pub fn evaluate_extras_and_python_version(
&self, &self,
extras: &HashSet<String>, extras: &HashSet<ExtraName>,
python_versions: &[Version], python_versions: &[Version],
) -> bool { ) -> bool {
match self { match self {
@ -1002,7 +1018,7 @@ impl MarkerTree {
pub fn evaluate_collect_warnings( pub fn evaluate_collect_warnings(
&self, &self,
env: &MarkerEnvironment, env: &MarkerEnvironment,
extras: &[&str], extras: &[ExtraName],
) -> (bool, Vec<(MarkerWarningKind, String, String)>) { ) -> (bool, Vec<(MarkerWarningKind, String, String)>) {
let mut warnings = Vec::new(); let mut warnings = Vec::new();
let mut reporter = |kind, warning, marker: &MarkerExpression| { let mut reporter = |kind, warning, marker: &MarkerExpression| {
@ -1131,7 +1147,7 @@ fn parse_marker_operator(cursor: &mut Cursor) -> Result<MarkerOperator, Pep508Er
start: cursor.pos(), start: cursor.pos(),
len: 1, len: 1,
input: cursor.to_string(), input: cursor.to_string(),
}) });
} }
Some((_, whitespace)) if whitespace.is_whitespace() => {} Some((_, whitespace)) if whitespace.is_whitespace() => {}
Some((pos, other)) => { Some((pos, other)) => {
@ -1142,7 +1158,7 @@ fn parse_marker_operator(cursor: &mut Cursor) -> Result<MarkerOperator, Pep508Er
start: pos, start: pos,
len: other.len_utf8(), len: other.len_utf8(),
input: cursor.to_string(), input: cursor.to_string(),
}) });
} }
}; };
cursor.eat_whitespace(); cursor.eat_whitespace();

View file

@ -37,7 +37,7 @@ impl PubGrubDependencies {
// If the requirement isn't relevant for the current platform, skip it. // If the requirement isn't relevant for the current platform, skip it.
if let Some(extra) = extra { if let Some(extra) = extra {
if !requirement.evaluate_markers(env, &[extra.as_ref()]) { if !requirement.evaluate_markers(env, std::slice::from_ref(extra)) {
continue; continue;
} }
} else { } else {
@ -89,7 +89,7 @@ impl PubGrubDependencies {
// If the requirement isn't relevant for the current platform, skip it. // If the requirement isn't relevant for the current platform, skip it.
if let Some(extra) = extra { if let Some(extra) = extra {
if !constraint.evaluate_markers(env, &[extra.as_ref()]) { if !constraint.evaluate_markers(env, std::slice::from_ref(extra)) {
continue; continue;
} }
} else { } else {