Use local versions of PEP 440 and PEP 508 crates (#32)

This PR modifies the PEP 440 and PEP 508 crates to pass CI, primarily by
fixing all lint violations.

We're also now using these crates in the workspace via `path`.
(Previously, we were still fetching them from Cargo.)
This commit is contained in:
Charlie Marsh 2023-10-06 20:16:44 -04:00 committed by GitHub
parent 4fcdb3c045
commit c8477991a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 359 additions and 1596 deletions

View file

@ -16,8 +16,6 @@
#![deny(missing_docs)]
mod marker;
#[cfg(feature = "modern")]
pub mod modern;
pub use marker::{
MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue,
@ -125,8 +123,8 @@ create_exception!(
);
/// A PEP 508 dependency specification
#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))]
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))]
pub struct Requirement {
/// The distribution name such as `numpy` in
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`
@ -159,18 +157,18 @@ impl Display for Requirement {
}
VersionOrUrl::Url(url) => {
// We add the space for markers later if necessary
write!(f, " @ {}", url)?;
write!(f, " @ {url}")?;
}
}
}
if let Some(marker) = &self.marker {
write!(f, " ; {}", marker)?;
write!(f, " ; {marker}")?;
}
Ok(())
}
}
/// https://github.com/serde-rs/serde/issues/908#issuecomment-298027413
/// <https://github.com/serde-rs/serde/issues/908#issuecomment-298027413>
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Requirement {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@ -182,7 +180,7 @@ impl<'de> Deserialize<'de> for Requirement {
}
}
/// https://github.com/serde-rs/serde/issues/1316#issue-332908452
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
#[cfg(feature = "serde")]
impl Serialize for Requirement {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@ -214,7 +212,7 @@ impl Requirement {
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`
#[getter]
pub fn marker(&self) -> Option<String> {
self.marker.as_ref().map(|m| m.to_string())
self.marker.as_ref().map(std::string::ToString::to_string)
}
/// Parses a PEP 440 string
@ -241,7 +239,7 @@ impl Requirement {
}
fn __repr__(&self) -> String {
format!(r#""{}""#, self)
format!(r#""{self}""#)
}
fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
@ -263,9 +261,10 @@ impl Requirement {
}
/// Returns whether the markers apply for the given environment
#[allow(clippy::needless_pass_by_value)]
#[pyo3(name = "evaluate_markers")]
pub fn py_evaluate_markers(&self, env: &MarkerEnvironment, extras: Vec<String>) -> bool {
self.evaluate_markers(env, extras)
self.evaluate_markers(env, &extras)
}
/// Returns whether the requirement would be satisfied, independent of environment markers, i.e.
@ -274,6 +273,7 @@ impl Requirement {
/// Note that unlike [Self::evaluate_markers] 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.
#[allow(clippy::needless_pass_by_value)]
#[pyo3(name = "evaluate_extras_and_python_version")]
pub fn py_evaluate_extras_and_python_version(
&self,
@ -281,28 +281,29 @@ impl Requirement {
python_versions: Vec<PyVersion>,
) -> bool {
self.evaluate_extras_and_python_version(
extras,
python_versions
&extras,
&python_versions
.into_iter()
.map(|py_version| py_version.0)
.collect(),
.collect::<Vec<_>>(),
)
}
/// Returns whether the markers apply for the given environment
#[allow(clippy::needless_pass_by_value)]
#[pyo3(name = "evaluate_markers_and_report")]
pub fn py_evaluate_markers_and_report(
&self,
env: &MarkerEnvironment,
extras: Vec<String>,
) -> (bool, Vec<(MarkerWarningKind, String, String)>) {
self.evaluate_markers_and_report(env, extras)
self.evaluate_markers_and_report(env, &extras)
}
}
impl Requirement {
/// Returns whether the markers apply for the given environment
pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: Vec<String>) -> bool {
pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[String]) -> bool {
if let Some(marker) = &self.marker {
marker.evaluate(
env,
@ -316,16 +317,16 @@ impl Requirement {
/// Returns whether the requirement would be satisfied, independent of environment markers, i.e.
/// if there is potentially an environment that could activate this requirement.
///
/// Note that unlike [Self::evaluate_markers] this does not perform any checks for bogus
/// Note that unlike [`Self::evaluate_markers`] 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<String>,
python_versions: Vec<Version>,
extras: &HashSet<String>,
python_versions: &[Version],
) -> bool {
if let Some(marker) = &self.marker {
marker.evaluate_extras_and_python_version(&extras, &python_versions)
marker.evaluate_extras_and_python_version(extras, python_versions)
} else {
true
}
@ -335,12 +336,15 @@ impl Requirement {
pub fn evaluate_markers_and_report(
&self,
env: &MarkerEnvironment,
extras: Vec<String>,
extras: &[String],
) -> (bool, Vec<(MarkerWarningKind, String, String)>) {
if let Some(marker) = &self.marker {
marker.evaluate_collect_warnings(
env,
&extras.iter().map(|x| x.as_str()).collect::<Vec<&str>>(),
&extras
.iter()
.map(std::string::String::as_str)
.collect::<Vec<&str>>(),
)
} else {
(true, Vec::new())
@ -448,11 +452,11 @@ impl<'a> CharIter<'a> {
while let Some(char) = self.peek_char() {
if !condition(char) {
break;
} else {
substring.push(char);
self.next();
len += 1;
}
substring.push(char);
self.next();
len += 1;
}
(substring, start, len)
}
@ -461,8 +465,7 @@ impl<'a> CharIter<'a> {
match self.next() {
None => Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected '{}', found end of dependency specification",
expected
"Expected '{expected}', found end of dependency specification"
)),
start: span_start,
len: 1,
@ -471,8 +474,7 @@ impl<'a> CharIter<'a> {
Some((_, value)) if value == expected => Ok(()),
Some((pos, other)) => Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected '{}', found '{}'",
expected, other
"Expected '{expected}', found '{other}'"
)),
start: pos,
len: 1,
@ -502,8 +504,7 @@ fn parse_name(chars: &mut CharIter) -> Result<String, Pep508Error> {
} else {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected package name starting with an alphanumeric character, found '{}'",
char
"Expected package name starting with an alphanumeric character, found '{char}'"
)),
start: index,
len: 1,
@ -528,8 +529,7 @@ fn parse_name(chars: &mut CharIter) -> Result<String, Pep508Error> {
if chars.peek().is_none() && matches!(char, '.' | '-' | '_') {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Package name must end with an alphanumeric character, not '{}'",
char
"Package name must end with an alphanumeric character, not '{char}'"
)),
start: index,
len: 1,
@ -544,9 +544,8 @@ fn parse_name(chars: &mut CharIter) -> Result<String, Pep508Error> {
/// parses extras in the `[extra1,extra2] format`
fn parse_extras(chars: &mut CharIter) -> Result<Option<Vec<String>>, Pep508Error> {
let bracket_pos = match chars.eat('[') {
Some(pos) => pos,
None => return Ok(None),
let Some(bracket_pos) = chars.eat('[') else {
return Ok(None);
};
let mut extras = Vec::new();
@ -568,14 +567,13 @@ fn parse_extras(chars: &mut CharIter) -> Result<Option<Vec<String>>, Pep508Error
match chars.next() {
// letterOrDigit
Some((_, alphanumeric @ ('a'..='z' | 'A'..='Z' | '0'..='9'))) => {
buffer.push(alphanumeric)
buffer.push(alphanumeric);
}
Some((pos, other)) => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected an alphanumeric character starting the extra name, found '{}'",
other
)),
"Expected an alphanumeric character starting the extra name, found '{other}'"
)),
start: pos,
len: 1,
input: chars.copy_chars(),
@ -598,7 +596,7 @@ fn parse_extras(chars: &mut CharIter) -> Result<Option<Vec<String>>, Pep508Error
Some((pos, char)) if char != ',' && char != ']' && !char.is_whitespace() => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Invalid character in extras name, expected an alphanumeric character, '-', '_', '.', ',' or ']', found '{}'", char
"Invalid character in extras name, expected an alphanumeric character, '-', '_', '.', ',' or ']', found '{char}'"
)),
start: pos,
len: 1,
@ -786,8 +784,7 @@ fn parse(chars: &mut CharIter) -> Result<Requirement, Pep508Error> {
Some(other) => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `{}`",
other
"Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `{other}`"
)),
start: chars.get_pos(),
len: 1,
@ -811,9 +808,9 @@ fn parse(chars: &mut CharIter) -> Result<Requirement, Pep508Error> {
if let Some((pos, char)) = chars.next() {
return Err(Pep508Error {
message: Pep508ErrorSource::String(if marker.is_none() {
format!(r#"Expected end of input or ';', found '{}'"#, char)
format!(r#"Expected end of input or ';', found '{char}'"#)
} else {
format!(r#"Expected end of input, found '{}'"#, char)
format!(r#"Expected end of input, found '{char}'"#)
}),
start: pos,
len: 1,
@ -854,7 +851,7 @@ pub fn python_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
Ok(())
}
/// Half of these tests are copied from https://github.com/pypa/packaging/pull/624
/// Half of these tests are copied from <https://github.com/pypa/packaging/pull/624>
#[cfg(test)]
mod tests {
use crate::marker::{

View file

@ -24,8 +24,8 @@ use std::str::FromStr;
use tracing::warn;
/// Ways in which marker evaluation can fail
#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))]
#[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>
@ -46,12 +46,14 @@ pub enum MarkerWarningKind {
#[cfg(feature = "pyo3")]
#[pymethods]
impl MarkerWarningKind {
#[allow(clippy::trivially_copy_pass_by_ref)]
fn __hash__(&self) -> u8 {
*self as u8
}
fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
op.matches(self.cmp(other))
#[allow(clippy::trivially_copy_pass_by_ref)]
fn __richcmp__(&self, other: Self, op: CompareOp) -> bool {
op.matches(self.cmp(&other))
}
}
@ -84,15 +86,15 @@ pub enum MarkerValueString {
ImplementationName,
/// `os_name`
OsName,
/// /// Deprecated `os.name` from https://peps.python.org/pep-0345/#environment-markers
/// 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
/// 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
/// Deprecated `platform.python_implementation` from <https://peps.python.org/pep-0345/#environment-markers>
PlatformPythonImplementationDeprecated,
/// `platform_release`
PlatformRelease,
@ -100,11 +102,11 @@ pub enum MarkerValueString {
PlatformSystem,
/// `platform_version`
PlatformVersion,
/// /// Deprecated `platform.version` from https://peps.python.org/pep-0345/#environment-markers
/// 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
/// Deprecated `sys.platform` from <https://peps.python.org/pep-0345/#environment-markers>
SysPlatformDeprecated,
}
@ -184,7 +186,7 @@ impl FromStr for MarkerValue {
"sys_platform" => Self::MarkerEnvString(MarkerValueString::SysPlatform),
"sys.platform" => Self::MarkerEnvString(MarkerValueString::SysPlatformDeprecated),
"extra" => Self::Extra,
_ => return Err(format!("Invalid key: {}", s)),
_ => return Err(format!("Invalid key: {s}")),
};
Ok(value)
}
@ -196,7 +198,7 @@ impl Display for MarkerValue {
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),
Self::QuotedString(value) => write!(f, "'{value}'"),
}
}
}
@ -267,7 +269,7 @@ impl FromStr for MarkerOperator {
{
Self::NotIn
}
other => return Err(format!("Invalid comparator: {}", other)),
other => return Err(format!("Invalid comparator: {other}")),
};
Ok(value)
}
@ -290,8 +292,8 @@ impl Display for MarkerOperator {
}
/// Helper type with a [Version] and its original text
#[cfg_attr(feature = "pyo3", pyclass(get_all, module = "pep508"))]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(get_all, module = "pep508"))]
pub struct StringVersion {
/// Original unchanged string
pub string: String,
@ -344,7 +346,7 @@ impl Deref for StringVersion {
/// <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)]
#[allow(missing_docs, clippy::unsafe_derive_deserialize)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "pyo3", pyclass(get_all, module = "pep508"))]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
@ -431,20 +433,17 @@ impl MarkerEnvironment {
let implementation_version =
StringVersion::from_str(implementation_version).map_err(|err| {
PyValueError::new_err(format!(
"implementation_version is not a valid PEP440 version: {}",
err
"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
"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
"python_version is not a valid PEP440 version: {err}"
))
})?;
Ok(Self {
@ -483,10 +482,10 @@ impl MarkerEnvironment {
info.getattr("major")?.extract::<usize>()?,
info.getattr("minor")?.extract::<usize>()?,
info.getattr("micro")?.extract::<usize>()?,
if kind != "final" {
format!("{}{}", kind, info.getattr("serial")?.extract::<usize>()?)
if kind == "final" {
String::new()
} else {
"".to_string()
format!("{}{}", kind, info.getattr("serial")?.extract::<usize>()?)
}
);
let python_full_version: String = platform.getattr("python_version")?.call0()?.extract()?;
@ -496,20 +495,17 @@ impl MarkerEnvironment {
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
"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
"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
"Broken python implementation, python_version is not a valid PEP440 version: {err}"
))
})?;
Ok(Self {
@ -546,7 +542,7 @@ pub struct MarkerExpression {
}
impl MarkerExpression {
/// Evaluate a <marker_value> <marker_op> <marker_value> expression
/// Evaluate a <`marker_value`> <`marker_op`> <`marker_value`> expression
fn evaluate(
&self,
env: &MarkerEnvironment,
@ -592,7 +588,7 @@ impl MarkerExpression {
Err(err) => {
reporter(
MarkerWarningKind::Pep440Error,
format!("Invalid operator/version combination: {}", err),
format!("Invalid operator/version combination: {err}"),
self,
);
return false;
@ -664,7 +660,7 @@ impl MarkerExpression {
Err(err) => {
reporter(
MarkerWarningKind::Pep440Error,
format!("Invalid operator/version combination: {}", err),
format!("Invalid operator/version combination: {err}"),
self,
);
return false;
@ -685,8 +681,7 @@ impl MarkerExpression {
// Not even pypa/packaging 22.0 supports this
// https://github.com/pypa/packaging/issues/632
reporter(MarkerWarningKind::StringStringComparison, format!(
"Comparing two quoted strings with each other doesn't make sense: {}, evaluating to false",
self
"Comparing two quoted strings with each other doesn't make sense: {self}, evaluating to false"
), self);
false
}
@ -703,7 +698,7 @@ impl MarkerExpression {
/// `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
/// Note that unlike [`Self::evaluate`] this does not perform any checks for bogus expressions but
/// will simply return true.
///
/// ```rust
@ -809,7 +804,7 @@ impl MarkerExpression {
MarkerOperator::GreaterThan => {
reporter(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {} and {} lexicographically", l_string, r_string),
format!("Comparing {l_string} and {r_string} lexicographically"),
self,
);
l_string > r_string
@ -817,7 +812,7 @@ impl MarkerExpression {
MarkerOperator::GreaterEqual => {
reporter(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {} and {} lexicographically", l_string, r_string),
format!("Comparing {l_string} and {r_string} lexicographically"),
self,
);
l_string >= r_string
@ -825,7 +820,7 @@ impl MarkerExpression {
MarkerOperator::LessThan => {
reporter(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {} and {} lexicographically", l_string, r_string),
format!("Comparing {l_string} and {r_string} lexicographically"),
self,
);
l_string < r_string
@ -833,7 +828,7 @@ impl MarkerExpression {
MarkerOperator::LessEqual => {
reporter(
MarkerWarningKind::LexicographicComparison,
format!("Comparing {} and {} lexicographically", l_string, r_string),
format!("Comparing {l_string} and {r_string} lexicographically"),
self,
);
l_string <= r_string
@ -841,7 +836,7 @@ impl MarkerExpression {
MarkerOperator::TildeEqual => {
reporter(
MarkerWarningKind::LexicographicComparison,
format!("Can't compare {} and {} with `~=`", l_string, r_string),
format!("Can't compare {l_string} and {r_string} with `~=`"),
self,
);
false
@ -880,8 +875,7 @@ impl FromStr for MarkerExpression {
if let Some((pos, unexpected)) = chars.next() {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Unexpected character '{}', expected end of input",
unexpected
"Unexpected character '{unexpected}', expected end of input"
)),
start: pos,
len: chars.chars.clone().count(),
@ -937,7 +931,7 @@ impl MarkerTree {
}
}
/// Same as [Self::evaluate], but instead of using logging to warn, you can pass your own
/// Same as [`Self::evaluate`], but instead of using logging to warn, you can pass your own
/// handler for warnings
pub fn evaluate_reporter(
&self,
@ -971,7 +965,7 @@ impl MarkerTree {
/// 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
/// 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(
@ -992,7 +986,7 @@ impl MarkerTree {
}
}
/// Same as [Self::evaluate], but instead of using logging to warn, you get a Vec with all
/// 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,
@ -1001,7 +995,7 @@ impl MarkerTree {
) -> (bool, Vec<(MarkerWarningKind, String, String)>) {
let mut warnings = Vec::new();
let mut reporter = |kind, warning, marker: &MarkerExpression| {
warnings.push((kind, warning, marker.to_string()))
warnings.push((kind, warning, marker.to_string()));
};
self.report_deprecated_options(&mut reporter);
let result = self.evaluate_reporter_impl(env, extras, &mut reporter);
@ -1066,12 +1060,12 @@ impl MarkerTree {
}
MarkerTree::And(expressions) => {
for expression in expressions {
expression.report_deprecated_options(reporter)
expression.report_deprecated_options(reporter);
}
}
MarkerTree::Or(expressions) => {
for expression in expressions {
expression.report_deprecated_options(reporter)
expression.report_deprecated_options(reporter);
}
}
}
@ -1082,13 +1076,13 @@ impl Display for MarkerTree {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let format_inner = |expression: &MarkerTree| {
if matches!(expression, MarkerTree::Expression(_)) {
format!("{}", expression)
format!("{expression}")
} else {
format!("({})", expression)
format!("({expression})")
}
};
match self {
MarkerTree::Expression(expression) => write!(f, "{}", expression),
MarkerTree::Expression(expression) => write!(f, "{expression}"),
MarkerTree::And(and_list) => f.write_str(
&and_list
.iter()
@ -1131,8 +1125,7 @@ fn parse_marker_operator(chars: &mut CharIter) -> Result<MarkerOperator, Pep508E
Some((pos, other)) => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected whitespace after 'not', found '{}'",
other
"Expected whitespace after 'not', found '{other}'"
)),
start: pos,
len: 1,
@ -1147,8 +1140,7 @@ fn parse_marker_operator(chars: &mut CharIter) -> Result<MarkerOperator, Pep508E
}
MarkerOperator::from_str(&operator).map_err(|_| Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected a valid marker operator (such as '>=' or 'not in'), found '{}'",
operator
"Expected a valid marker operator (such as '>=' or 'not in'), found '{operator}'"
)),
start,
len,
@ -1156,10 +1148,10 @@ fn parse_marker_operator(chars: &mut CharIter) -> Result<MarkerOperator, Pep508E
})
}
/// 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'
/// 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(chars: &mut CharIter) -> Result<MarkerValue, Pep508Error> {
// > 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
@ -1189,8 +1181,7 @@ fn parse_marker_value(chars: &mut CharIter) -> Result<MarkerValue, Pep508Error>
});
MarkerValue::from_str(&key).map_err(|_| Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected a valid marker name, found '{}'",
key
"Expected a valid marker name, found '{key}'"
)),
start,
len,
@ -1303,8 +1294,7 @@ pub(crate) fn parse_markers_impl(chars: &mut CharIter) -> Result<MarkerTree, Pep
// character was neither "and" nor "or"
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Unexpected character '{}', expected 'and', 'or' or end of input",
unexpected
"Unexpected character '{unexpected}', expected 'and', 'or' or end of input"
)),
start: pos,
len: chars.chars.clone().count(),
@ -1337,14 +1327,14 @@ mod test {
let v37 = StringVersion::from_str("3.7").unwrap();
MarkerEnvironment {
implementation_name: "".to_string(),
implementation_name: String::new(),
implementation_version: v37.clone(),
os_name: "linux".to_string(),
platform_machine: "".to_string(),
platform_python_implementation: "".to_string(),
platform_release: "".to_string(),
platform_system: "".to_string(),
platform_version: "".to_string(),
platform_machine: String::new(),
platform_python_implementation: String::new(),
platform_release: String::new(),
platform_system: String::new(),
platform_version: String::new(),
python_full_version: v37.clone(),
python_version: v37,
sys_platform: "linux".to_string(),
@ -1383,9 +1373,7 @@ mod test {
assert_eq!(
MarkerTree::from_str(a).unwrap(),
MarkerTree::from_str(b).unwrap(),
"{} {}",
a,
b
"{a} {b}"
);
}
}
@ -1394,14 +1382,14 @@ mod test {
fn test_marker_evaluation() {
let v27 = StringVersion::from_str("2.7").unwrap();
let env27 = MarkerEnvironment {
implementation_name: "".to_string(),
implementation_name: String::new(),
implementation_version: v27.clone(),
os_name: "linux".to_string(),
platform_machine: "".to_string(),
platform_python_implementation: "".to_string(),
platform_release: "".to_string(),
platform_system: "".to_string(),
platform_version: "".to_string(),
platform_machine: String::new(),
platform_python_implementation: String::new(),
platform_release: String::new(),
platform_system: String::new(),
platform_version: String::new(),
python_full_version: v27.clone(),
python_version: v27,
sys_platform: "linux".to_string(),

View file

@ -1,362 +0,0 @@
//! WIP Draft for a poetry/cargo like, modern dependency specification
//!
//! This still needs
//! * Better VersionSpecifier (e.g. allowing `^1.19`) and it's sentry integration
//! * PEP 440/PEP 508 translation
//! * a json schema
#![cfg(feature = "modern")]
use crate::MarkerValue::QuotedString;
use crate::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValue, Requirement, VersionOrUrl};
use anyhow::{bail, format_err, Context};
use once_cell::sync::Lazy;
use pep440_rs::{Operator, Pep440Error, Version, VersionSpecifier, VersionSpecifiers};
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
use url::Url;
/// Shared fields for version/git/file/path/url dependencies (`optional`, `extras`, `markers`)
#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize)]
pub struct RequirementModernCommon {
/// Whether this is an optional dependency. This is inverted from PEP 508 extras where the
/// requirements has the extras attached, as here the extras has a table where each extra
/// says which optional dependencies it activates
#[serde(default)]
pub optional: bool,
/// The list of extras <https://packaging.python.org/en/latest/tutorials/installing-packages/#installing-extras>
pub extras: Option<Vec<String>>,
/// The list of markers <https://peps.python.org/pep-0508/#environment-markers>.
/// Note that this will not accept extras.
///
/// TODO: Deserialize into `MarkerTree` that does not accept the extras key
pub markers: Option<String>,
}
/// Instead of only PEP 440 specifier, you can also set a single version (exact) or TODO use
/// the semver caret
#[derive(Eq, PartialEq, Debug, Clone, Serialize)]
pub enum VersionSpecifierModern {
/// e.g. `4.12.1-beta.1`
Version(Version),
/// e.g. `== 4.12.1-beta.1` or `>=3.8,<4.0`
VersionSpecifier(VersionSpecifiers),
}
impl VersionSpecifierModern {
/// `4.12.1-beta.1` -> `== 4.12.1-beta.1`
/// `== 4.12.1-beta.1` -> `== 4.12.1-beta.1`
/// `>=3.8,<4.0` -> `>=3.8,<4.0`
/// TODO: `^1.19` -> `>=1.19,<2.0`
pub fn to_pep508_specifier(&self) -> VersionSpecifiers {
match self {
// unwrapping is safe here because we're using Operator::Equal
VersionSpecifierModern::Version(version) => {
[VersionSpecifier::new(Operator::Equal, version.clone(), false).unwrap()]
.into_iter()
.collect()
}
VersionSpecifierModern::VersionSpecifier(version_specifiers) => {
version_specifiers.clone()
}
}
}
}
impl FromStr for VersionSpecifierModern {
/// TODO: Modern needs it's own error type
type Err = Pep440Error;
/// dispatching between just a version and a version specifier set
fn from_str(s: &str) -> Result<Self, Self::Err> {
// If it starts with
if s.trim_start().starts_with(|x: char| x.is_ascii_digit()) {
Ok(Self::Version(Version::from_str(s).map_err(|err| {
// TODO: Fix this in pep440_rs
Pep440Error {
message: err,
line: s.to_string(),
start: 0,
width: 1,
}
})?))
} else if s.starts_with('^') {
todo!("TODO caret operator is not supported yet");
} else {
Ok(Self::VersionSpecifier(VersionSpecifiers::from_str(s)?))
}
}
}
/// https://github.com/serde-rs/serde/issues/908#issuecomment-298027413
impl<'de> Deserialize<'de> for VersionSpecifierModern {
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)
}
}
/// WIP Draft for a poetry/cargo like, modern dependency specification
#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RequirementModern {
/// e.g. `numpy = "1.24.1"`
Dependency(VersionSpecifierModern),
/// e.g. `numpy = { version = "1.24.1" }` or `django-anymail = { version = "1.24.1", extras = ["sendgrid"], optional = true }`
LongDependency {
/// e.g. `1.2.3.beta1`
version: VersionSpecifierModern,
#[serde(flatten)]
#[allow(missing_docs)]
common: RequirementModernCommon,
},
/// e.g. `tqdm = { git = "https://github.com/tqdm/tqdm", rev = "0bb91857eca0d4aea08f66cf1c8949abe0cd6b7a" }`
GitDependency {
/// URL of the git repository e.g. `https://github.com/tqdm/tqdm`
git: Url,
/// The git branch to use
branch: Option<String>,
/// The git revision to use. Can be the short revision (`0bb9185`) or the long revision
/// (`0bb91857eca0d4aea08f66cf1c8949abe0cd6b7a`)
rev: Option<String>,
#[serde(flatten)]
#[allow(missing_docs)]
common: RequirementModernCommon,
},
/// e.g. `tqdm = { file = "tqdm-4.65.0-py3-none-any.whl" }`
FileDependency {
/// Path to a source distribution (e.g. `tqdm-4.65.0.tar.gz`) or wheel (e.g. `tqdm-4.65.0-py3-none-any.whl`)
file: String,
#[serde(flatten)]
#[allow(missing_docs)]
common: RequirementModernCommon,
},
/// Path to a directory with source distributions and/or wheels e.g.
/// `scilib_core = { path = "build_wheels/scilib_core/" }`.
///
/// Use this option if you e.g. have multiple platform platform dependent wheels or want to
/// have a fallback to a source distribution for you wheel.
PathDependency {
/// e.g. `dist/`, `target/wheels` or `vendored`
path: String,
#[serde(flatten)]
#[allow(missing_docs)]
common: RequirementModernCommon,
},
/// e.g. `jax = { url = "https://storage.googleapis.com/jax-releases/cuda112/jaxlib-0.1.64+cuda112-cp39-none-manylinux2010_x86_64.whl" }`
UrlDependency {
/// URL to a source distribution or wheel. The file available there must be named
/// appropriately for a source distribution or a wheel.
url: Url,
#[serde(flatten)]
#[allow(missing_docs)]
common: RequirementModernCommon,
},
}
/// Adopted from the grammar at <https://peps.python.org/pep-0508/#extras>
static EXTRA_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[a-zA-Z0-9]([-_.]*[a-zA-Z0-9])*$").unwrap());
impl RequirementModern {
/// Check the things that serde doesn't check, namely that extra names are valid
pub fn check(&self) -> anyhow::Result<()> {
match self {
Self::LongDependency { common, .. }
| Self::GitDependency { common, .. }
| Self::FileDependency { common, .. }
| Self::PathDependency { common, .. }
| Self::UrlDependency { common, .. } => {
if let Some(extras) = &common.extras {
for extra in extras {
if !EXTRA_REGEX.is_match(extra) {
bail!("Not a valid extra name: '{}'", extra)
}
}
}
}
_ => {}
}
Ok(())
}
/// WIP Converts the modern format to PEP 508
pub fn to_pep508(
&self,
name: &str,
extras: &HashMap<String, Vec<String>>,
) -> Result<Requirement, anyhow::Error> {
let default = RequirementModernCommon {
optional: false,
extras: None,
markers: None,
};
let common = match self {
RequirementModern::Dependency(..) => &default,
RequirementModern::LongDependency { common, .. }
| RequirementModern::GitDependency { common, .. }
| RequirementModern::FileDependency { common, .. }
| RequirementModern::PathDependency { common, .. }
| RequirementModern::UrlDependency { common, .. } => common,
};
let marker = if common.optional {
// invert the extras table from the modern format
// extra1 -> optional_dep1, optional_dep2, ...
// to the PEP 508 format
// optional_dep1; extra == "extra1" or extra == "extra2"
let dep_markers = extras
.iter()
.filter(|(_marker, dependencies)| dependencies.contains(&name.to_string()))
.map(|(marker, _dependencies)| {
MarkerTree::Expression(MarkerExpression {
l_value: MarkerValue::Extra,
operator: MarkerOperator::Equal,
r_value: QuotedString(marker.to_string()),
})
})
.collect();
// any of these extras activates the dependency -> or clause
let dep_markers = MarkerTree::Or(dep_markers);
let joined_marker = if let Some(user_markers) = &common.markers {
let user_markers = MarkerTree::from_str(user_markers)
.context("TODO: parse this in serde already")?;
// but the dependency needs to be activated and match the other markers
// -> and clause
MarkerTree::And(vec![user_markers, dep_markers])
} else {
dep_markers
};
Some(joined_marker)
} else {
None
};
if let Some(extras) = &common.extras {
debug_assert!(extras.iter().all(|extra| EXTRA_REGEX.is_match(extra)));
}
let version_or_url = match self {
RequirementModern::Dependency(version) => {
VersionOrUrl::VersionSpecifier(version.to_pep508_specifier())
}
RequirementModern::LongDependency { version, .. } => {
VersionOrUrl::VersionSpecifier(version.to_pep508_specifier())
}
RequirementModern::GitDependency {
git, branch, rev, ..
} => {
// TODO: Read https://peps.python.org/pep-0440/#direct-references properly
// set_scheme doesn't like us adding `git+` to https, therefore this hack
let mut url =
Url::parse(&format!("git+{}", git)).expect("TODO: Better url validation");
match (branch, rev) {
(Some(_branch), Some(_rev)) => {
bail!("You can set both branch and rev (for {})", name)
}
(Some(branch), None) => url.set_path(&format!("{}@{}", url.path(), branch)),
(None, Some(rev)) => url.set_path(&format!("{}@{}", url.path(), rev)),
(None, None) => {}
}
VersionOrUrl::Url(url)
}
RequirementModern::FileDependency { file, .. } => VersionOrUrl::Url(
Url::from_file_path(file)
.map_err(|()| format_err!("File must be absolute (for {})", name))?,
),
RequirementModern::PathDependency { path, .. } => VersionOrUrl::Url(
Url::from_directory_path(path)
.map_err(|()| format_err!("Path must be absolute (for {})", name))?,
),
RequirementModern::UrlDependency { url, .. } => VersionOrUrl::Url(url.clone()),
};
Ok(Requirement {
name: name.to_string(),
extras: common.extras.clone(),
version_or_url: Some(version_or_url),
marker,
})
}
}
#[cfg(test)]
mod test {
use crate::modern::{RequirementModern, VersionSpecifierModern};
use crate::Requirement;
use indoc::indoc;
use pep440_rs::VersionSpecifiers;
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
#[test]
fn test_basic() {
let deps: HashMap<String, RequirementModern> =
toml::from_str(r#"numpy = "==1.19""#).unwrap();
assert_eq!(
deps["numpy"],
RequirementModern::Dependency(VersionSpecifierModern::VersionSpecifier(
VersionSpecifiers::from_str("==1.19").unwrap()
))
);
assert_eq!(
deps["numpy"].to_pep508("numpy", &HashMap::new()).unwrap(),
Requirement::from_str("numpy== 1.19").unwrap()
);
}
#[test]
fn test_conversion() {
#[derive(Deserialize)]
struct PyprojectToml {
// BTreeMap to keep the order
#[serde(rename = "modern-dependencies")]
modern_dependencies: BTreeMap<String, RequirementModern>,
extras: HashMap<String, Vec<String>>,
}
let pyproject_toml = indoc! {r#"
[modern-dependencies]
pydantic = "1.10.5"
numpy = ">=1.24.2, <2.0.0"
pandas = { version = ">=1.5.3, <2.0.0" }
flask = { version = "2.2.3 ", extras = ["dotenv"], optional = true }
tqdm = { git = "https://github.com/tqdm/tqdm", rev = "0bb91857eca0d4aea08f66cf1c8949abe0cd6b7a" }
jax = { url = "https://storage.googleapis.com/jax-releases/cuda112/jaxlib-0.1.64+cuda112-cp39-none-manylinux2010_x86_64.whl" }
zstandard = { file = "/home/ferris/wheels/zstandard/zstandard-0.20.0.tar.gz" }
h5py = { path = "/home/ferris/wheels/h5py/" }
[extras]
internet = ["flask"]
"#
};
let deps: PyprojectToml = toml::from_str(pyproject_toml).unwrap();
let actual: Vec<String> = deps
.modern_dependencies
.iter()
.map(|(name, spec)| spec.to_pep508(name, &deps.extras).unwrap().to_string())
.collect();
let expected: Vec<String> = vec![
"flask[dotenv] ==2.2.3 ; extra == 'internet'".to_string(),
"h5py @ file:///home/ferris/wheels/h5py/".to_string(),
"jax @ https://storage.googleapis.com/jax-releases/cuda112/jaxlib-0.1.64+cuda112-cp39-none-manylinux2010_x86_64.whl".to_string(),
"numpy >=1.24.2, <2.0.0".to_string(),
"pandas >=1.5.3, <2.0.0".to_string(),
"pydantic ==1.10.5".to_string(),
"tqdm @ git+https://github.com/tqdm/tqdm@0bb91857eca0d4aea08f66cf1c8949abe0cd6b7a".to_string(),
"zstandard @ file:///home/ferris/wheels/zstandard/zstandard-0.20.0.tar.gz".to_string()
];
assert_eq!(actual, expected)
}
}