pyreq_rs/
requirements.rs

1use std::{cmp::Ordering, fmt::Display};
2
3use crate::parser::version::version_scheme;
4
5#[cfg(test)]
6mod tests;
7
8// version_cmp
9#[derive(Debug, PartialEq, Clone, Copy)]
10pub enum Comparison {
11    LessThan,
12    LessThanOrEqual,
13    NotEqual,
14    Equal,
15    GreaterThanOrEqual,
16    GreaterThan,
17    CompatibleRelease,
18    ArbitraryEqual,
19}
20
21impl TryFrom<&str> for Comparison {
22    type Error = ();
23
24    fn try_from(value: &str) -> Result<Self, Self::Error> {
25        match value {
26            "<" => Ok(Self::LessThan),
27            "<=" => Ok(Self::LessThanOrEqual),
28            "!=" => Ok(Self::NotEqual),
29            "==" => Ok(Self::Equal),
30            ">=" => Ok(Self::GreaterThanOrEqual),
31            ">" => Ok(Self::GreaterThan),
32            "~=" => Ok(Self::CompatibleRelease),
33            "===" => Ok(Self::ArbitraryEqual),
34            _ => Err(()),
35        }
36    }
37}
38
39// marker_op
40#[derive(Debug, PartialEq)]
41pub enum MarkerOp {
42    Comparison(Comparison),
43    In,
44    NotIn,
45}
46
47// and 优先级大于 or
48#[derive(Debug, PartialEq)]
49pub enum MarkerExpr {
50    Basic(String, MarkerOp, String),
51    And(Box<Self>, Box<Self>),
52    Or(Box<Self>, Box<Self>),
53}
54
55impl From<Comparison> for MarkerOp {
56    fn from(c: Comparison) -> Self {
57        Self::Comparison(c)
58    }
59}
60
61#[derive(Debug, PartialEq)]
62pub enum VersionControlSystem {
63    Git,
64    Mercurial,
65    Subversion,
66    Bazaar,
67    Unknown,
68}
69
70// see regex for VersionSpecifier at https://github.com/pypa/packaging/blob/main/src/packaging/specifiers.py
71#[derive(Debug, PartialEq)]
72pub struct VersionSpec(pub Comparison, pub String);
73
74impl From<(Comparison, String)> for VersionSpec {
75    fn from((c, v): (Comparison, String)) -> Self {
76        Self(c, v)
77    }
78}
79
80impl VersionSpec {
81    // refer to contains at https://github.com/pypa/packaging/blob/main/src/packaging/specifiers.py
82    // 该方法默认允许pre-releases
83    pub fn contains(&self, version: &str) -> bool {
84        if let Ok((_, v)) = version_scheme(version) {
85            match self.0 {
86                Comparison::CompatibleRelease => self.compare_compatible(&v, &self.1),
87                Comparison::Equal => self.compare_equal(&v, &self.1),
88                Comparison::NotEqual => self.compare_not_equal(&v, &self.1),
89                Comparison::LessThanOrEqual => self.compare_less_than_equal(&v, &self.1),
90                Comparison::GreaterThanOrEqual => self.compare_greater_than_equal(&v, &self.1),
91                Comparison::LessThan => self.compare_less_than(&v, &self.1),
92                Comparison::GreaterThan => self.compare_greater_than(&v, &self.1),
93                Comparison::ArbitraryEqual => self.compare_arbitrary(&v, &self.1),
94            }
95        } else {
96            // invalid version, just return false
97            false
98        }
99    }
100
101    // ~=2.2 is equivalent to >=2.2,==2.*
102    fn compare_compatible(&self, prospective: &Version, spec: &str) -> bool {
103        if let Ok(("", v)) = version_scheme(spec) {
104            // ignore suffix segments(only contains epoch and release)
105            self.compare_greater_than_equal(prospective, spec)
106                && self.compare_equal(prospective, &v.prefix_str())
107        } else {
108            // 按道理spec必能解析为Version,但这里做个容错
109            false
110        }
111    }
112    // spec中允许包含wildcard(prefix match)和local versions
113    fn compare_equal(&self, prospective: &Version, spec: &str) -> bool {
114        // prefix matching
115        // 按解析的语法, spec只能是[epoch]release.*的格式
116        // 在判断prefix match忽略prospective的local segment
117        // 我这里的实现跟python不同,没用version_split,是先判断epoch是否相等,再判断release
118        if spec.ends_with(".*") {
119            if let Ok(("", spec_v)) = version_scheme(&spec[..spec.len() - 2]) {
120                if prospective.epoch != spec_v.epoch {
121                    return false;
122                }
123                // 0-pad the prospective version
124                // python中的_pad_version是在_version_split数组的release后边加"0"元素,使两个数组长度相同
125                for i in 0..prospective.release.len().min(spec_v.release.len()) {
126                    if prospective.release[i] != spec_v.release[i] {
127                        return false;
128                    }
129                }
130                // prospective.release更多不用处理,因为只要前缀匹配就可以
131                // spec_v.release更多的情况, 多出来的部分必须全是0(符合python中的0-pad)
132                if spec_v.release.len() > prospective.release.len() {
133                    return spec_v.release[prospective.release.len()..spec_v.release.len()]
134                        .iter()
135                        .all(|&i| i == 0);
136                }
137                true
138            } else {
139                false
140            }
141        } else {
142            if let Ok(("", mut spec_v)) = version_scheme(spec) {
143                if spec_v.local.is_none() && prospective.local.is_some() {
144                    spec_v.local = prospective.local.clone();
145                }
146                prospective.eq(&spec_v)
147            } else {
148                false
149            }
150        }
151    }
152    fn compare_not_equal(&self, prospective: &Version, spec: &str) -> bool {
153        !self.compare_equal(prospective, spec)
154    }
155    fn compare_less_than_equal(&self, prospective: &Version, spec: &str) -> bool {
156        if let Ok(("", spec_v)) = version_scheme(spec) {
157            prospective.to_public() <= spec_v
158        } else {
159            false
160        }
161    }
162    fn compare_greater_than_equal(&self, prospective: &Version, spec: &str) -> bool {
163        if let Ok(("", spec_v)) = version_scheme(spec) {
164            prospective.to_public() >= spec_v
165        } else {
166            false
167        }
168    }
169    fn compare_less_than(&self, prospective: &Version, spec: &str) -> bool {
170        if let Ok(("", spec_v)) = version_scheme(spec) {
171            if !(prospective < &spec_v) {
172                return false;
173            }
174            if !spec_v.is_prerelease() && prospective.is_prerelease() {
175                if prospective.to_base() == spec_v.to_base() {
176                    return false;
177                }
178            }
179            true
180        } else {
181            false
182        }
183    }
184    fn compare_greater_than(&self, prospective: &Version, spec: &str) -> bool {
185        if let Ok(("", spec_v)) = version_scheme(spec) {
186            if !(prospective > &spec_v) {
187                return false;
188            }
189            if !spec_v.is_postrelease() && prospective.is_postrelease() {
190                if prospective.to_base() == spec_v.to_base() {
191                    return false;
192                }
193            }
194            if prospective.local.is_some() {
195                if prospective.to_base() == spec_v.to_base() {
196                    return false;
197                }
198            }
199            true
200        } else {
201            false
202        }
203    }
204    fn compare_arbitrary(&self, prospective: &Version, spec: &str) -> bool {
205        prospective.to_string().eq_ignore_ascii_case(spec)
206    }
207}
208
209#[derive(Debug, PartialEq, Default)]
210pub struct RequirementSpecifier {
211    pub name: String,
212    pub extras: Vec<String>,
213    pub version_specs: Vec<VersionSpec>,
214    pub urlspec: Option<String>,
215    pub marker_expr: Option<MarkerExpr>,
216}
217
218impl RequirementSpecifier {
219    pub fn contains_version(&self, version: &str) -> bool {
220        self.version_specs.iter().all(|spec| spec.contains(version))
221    }
222}
223
224#[derive(Debug, Clone, Eq)]
225pub enum LocalVersionPart {
226    Num(u64),
227    LowerStr(String),
228}
229
230// https://peps.python.org/pep-0440/#local-version-identifiers
231impl Ord for LocalVersionPart {
232    fn cmp(&self, other: &Self) -> Ordering {
233        match self {
234            Self::Num(self_n) => match other {
235                Self::Num(other_n) => self_n.cmp(other_n),
236                Self::LowerStr(_) => Ordering::Greater,
237            },
238            Self::LowerStr(self_s) => match other {
239                Self::Num(_) => Ordering::Less,
240                Self::LowerStr(other_s) => self_s.cmp(other_s),
241            },
242        }
243    }
244}
245
246impl PartialOrd for LocalVersionPart {
247    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
248        Some(self.cmp(other))
249    }
250}
251
252impl PartialEq for LocalVersionPart {
253    fn eq(&self, other: &Self) -> bool {
254        self.cmp(other) == Ordering::Equal
255    }
256}
257
258impl Display for LocalVersionPart {
259    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        match self {
261            LocalVersionPart::LowerStr(s) => write!(f, "{}", s),
262            LocalVersionPart::Num(n) => write!(f, "{}", n),
263        }
264    }
265}
266
267// this is a version identifier, defined in pep 440, it is not the same as the string used in VersionSpec
268// public version identifier = [N!]N(.N)*[{a|b|rc}N][.postN][.devN]
269// local version identifier = <public version identifier>[+<local version label>]
270#[derive(Debug, Default, Eq)]
271pub struct Version {
272    pub epoch: u64,
273    pub release: Vec<u64>,
274    pub pre: Option<(String, u64)>,
275    pub post: Option<(String, u64)>,
276    pub dev: Option<(String, u64)>,
277    pub local: Option<Vec<LocalVersionPart>>,
278}
279
280// permitted suffix and relative ordering
281// Within a numeric release: .devN, aN, bN, rcN, <no suffix>, .postN
282// within a pre-release: .devN, <no suffix>, .postN
283// within a post-release: .devN, <no suffix>
284// Within a pre-release, post-release or development release segment with a shared prefix, ordering MUST be by the value of the numeric component.
285// 借鉴_cmpkey的方式排序
286// _cmpkey at https://github.com/pypa/packaging/blob/main/src/packaging/version.py
287impl Ord for Version {
288    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
289        self.cmpkey().cmp(&other.cmpkey())
290    }
291}
292
293impl PartialOrd for Version {
294    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
295        Some(self.cmp(other))
296    }
297}
298
299impl PartialEq for Version {
300    fn eq(&self, other: &Self) -> bool {
301        self.cmp(other) == Ordering::Equal
302    }
303}
304
305static NEGATIVE_INFINITY_LOCAL: Vec<LocalVersionPart> = vec![];
306static INFINITY_PRE_POST_DEV: (&'static str, u64) = ("~", u64::MAX);
307static NEGATIVE_INFINITY_PRE_POST_DEV: (&'static str, u64) = ("!", 0);
308impl Version {
309    pub fn cmpkey(
310        &self,
311    ) -> (
312        u64,
313        Vec<u64>,
314        (&str, u64),
315        (&str, u64),
316        (&str, u64),
317        &Vec<LocalVersionPart>,
318    ) {
319        let pre = if self.pre.is_none() && self.post.is_none() && self.dev.is_some() {
320            NEGATIVE_INFINITY_PRE_POST_DEV
321        } else {
322            match self.pre {
323                None => INFINITY_PRE_POST_DEV,
324                Some((ref l, n)) => (l.as_str(), n),
325            }
326        };
327        let post = match self.post {
328            None => NEGATIVE_INFINITY_PRE_POST_DEV,
329            Some((ref l, n)) => (l.as_str(), n),
330        };
331        let dev = match self.dev {
332            None => INFINITY_PRE_POST_DEV,
333            Some((ref l, n)) => (l.as_str(), n),
334        };
335        let local = match self.local {
336            None => &NEGATIVE_INFINITY_LOCAL,
337            Some(ref v) => v,
338        };
339        (
340            self.epoch,
341            self.release_without_trailing_zero(),
342            pre,
343            post,
344            dev,
345            local,
346        )
347    }
348
349    // 用于Version.cmp
350    pub fn release_without_trailing_zero(&self) -> Vec<u64> {
351        self.release
352            .iter()
353            .rev()
354            .skip_while(|&&r| r == 0)
355            .collect::<Vec<&u64>>()
356            .iter()
357            .rev()
358            .map(|&&x| x)
359            .collect()
360    }
361
362    // 用于compare_compatible
363    pub fn prefix_str(&self) -> String {
364        let mut parts = String::new();
365        // epoch
366        if self.epoch != 0 {
367            parts.push_str(&format!("{}!", self.epoch));
368        }
369        // 忽略 release 最后一位,用'.*'替代
370        if self.release.len() > 1 {
371            for i in &self.release[..self.release.len() - 1] {
372                parts.push_str(&format!("{}.", i));
373            }
374            parts.truncate(parts.len() - 1);
375        }
376        parts.push_str(".*");
377        parts
378    }
379
380    pub fn is_prerelease(&self) -> bool {
381        self.dev.is_some() || self.pre.is_some()
382    }
383
384    pub fn is_postrelease(&self) -> bool {
385        self.post.is_some()
386    }
387
388    // The public portion of the version.(without local)
389    // public_str = ver.public().to_string()
390    pub fn to_public(&self) -> Self {
391        Self {
392            epoch: self.epoch.clone(),
393            release: self.release.clone(),
394            pre: self.pre.clone(),
395            post: self.post.clone(),
396            dev: self.dev.clone(),
397            local: None,
398        }
399    }
400
401    pub fn to_base(&self) -> Self {
402        Self {
403            epoch: self.epoch.clone(),
404            release: self.release.clone(),
405            pre: None,
406            post: None,
407            dev: None,
408            local: None,
409        }
410    }
411
412    pub fn public_str(&self) -> String {
413        self.canonicalize_str(false, false)
414    }
415
416    // canonicalize_version at https://github.com/pypa/packaging/blob/main/src/packaging/utils.py
417    // strip_trailing_zero: 不包含release后边的'.0'. 用VersionSpec的哈希和相等比较,见Specifier中的_canonical_spec, __hash__, __eq__. https://github.com/pypa/packaging/blob/main/src/packaging/specifiers.py
418    // with_local: public_str 不包含local version part
419    pub fn canonicalize_str(&self, strip_trailing_zero: bool, with_local: bool) -> String {
420        let mut parts = String::new();
421        // epoch
422        if self.epoch != 0 {
423            parts.push_str(&format!("{}!", self.epoch));
424        }
425        // release
426        if strip_trailing_zero {
427            for i in self
428                .release
429                .iter()
430                .rev()
431                .skip_while(|&&r| r == 0)
432                .collect::<Vec<&u64>>()
433                .iter()
434                .rev()
435            {
436                parts.push_str(&format!("{}.", i));
437            }
438        } else {
439            for i in self.release.iter() {
440                parts.push_str(&format!("{}.", i));
441            }
442        }
443        parts.truncate(parts.len() - 1);
444        // pre-release
445        if let Some((l, n)) = self.pre.as_ref() {
446            parts.push_str(&format!("{}{}", l, n));
447        }
448        // post-release
449        if let Some((_, n)) = self.post.as_ref() {
450            parts.push_str(&format!(".post{}", n));
451        }
452        // dev-release
453        if let Some((_, n)) = self.dev.as_ref() {
454            parts.push_str(&format!(".dev{}", n));
455        }
456        // local version segment
457        if with_local {
458            if let Some(local) = self.local.as_ref() {
459                parts.push_str("+");
460                for i in local.iter() {
461                    parts.push_str(&format!("{}.", i));
462                }
463                parts.truncate(parts.len() - 1);
464            }
465        }
466        parts
467    }
468}
469
470// refer to Version.__str__ from https://github.com/pypa/packaging/blob/main/src/packaging/version.py
471impl Display for Version {
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473        write!(f, "{}", self.canonicalize_str(false, true))
474    }
475}