pep440_rs-0.3.12/.cargo_vcs_info.json0000644000000001360000000000100126370ustar { "git": { "sha1": "74c297a3fff9c434aee3e92216eb95f2b84fd4d6" }, "path_in_vcs": "" }pep440_rs-0.3.12/Cargo.toml0000644000000026640000000000100106450ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "pep440_rs" version = "0.3.12" include = [ "/src", "Changelog.md", "License-Apache", "License-BSD", "Readme.md", "pyproject.toml", ] description = "A library for python version numbers and specifiers, implementing PEP 440" readme = "Readme.md" license = "Apache-2.0 OR BSD-2-Clause" repository = "https://github.com/konstin/pep440-rs" [lib] name = "pep440_rs" crate-type = [ "rlib", "cdylib", ] [dependencies.lazy_static] version = "1.4.0" [dependencies.pyo3] version = "0.19" features = [ "extension-module", "abi3-py37", ] optional = true [dependencies.regex] version = "1.8.1" features = [ "std", "perf", "unicode-case", "unicode-perl", ] default-features = false [dependencies.serde] version = "1.0.162" features = ["derive"] optional = true [dependencies.tracing] version = "0.1.37" optional = true [dependencies.unicode-width] version = "0.1.10" [dev-dependencies.indoc] version = "2.0.1" pep440_rs-0.3.12/Cargo.toml.orig000064400000000000000000000016161046102023000143220ustar 00000000000000[package] name = "pep440_rs" version = "0.3.12" description = "A library for python version numbers and specifiers, implementing PEP 440" edition = "2021" include = ["/src", "Changelog.md", "License-Apache", "License-BSD", "Readme.md", "pyproject.toml"] # Same license as pypa/packaging where the tests are from license = "Apache-2.0 OR BSD-2-Clause" repository = "https://github.com/konstin/pep440-rs" readme = "Readme.md" [lib] name = "pep440_rs" crate-type = ["rlib", "cdylib"] [dependencies] lazy_static = "1.4.0" pyo3 = { version = "0.19", optional = true, features = ["extension-module", "abi3-py37"] } regex = { version = "1.8.1", default-features = false, features = ["std", "perf", "unicode-case", "unicode-perl"] } serde = { version = "1.0.162", features = ["derive"], optional = true } tracing = { version = "0.1.37", optional = true } unicode-width = "0.1.10" [dev-dependencies] indoc = "2.0.1" pep440_rs-0.3.12/Changelog.md000064400000000000000000000022121046102023000136350ustar 00000000000000## 0.3.12 * Implement `FromPyObject` for `Version` ## 0.3.11 * CI fix ## 0.3.10 * Update pyo3 to 0.19 and maturin to 1.0 ## 0.3.7 * Add `major()`, `minor()` and `micro()` to `Version` by ischaojie ([#9](https://github.com/konstin/pep440-rs/pull/9)) * ## 0.3.6 * Fix Readme display ## 0.3.5 * Make string serialization look more like poetry's * Implement `__hash__` for `VersionSpecifier` ## 0.3.4 * Python bindings for `VersionSpecifiers` ## 0.3.3 * Implement `Display` for `VersionSpecifiers` ## 0.3.2 * Expose `VersionSpecifier().operator` and `VersionSpecifier().version` to Python ## 0.3.1 * Expose `Version` from `PyVersion` ## 0.3.0 * Introduced a `PyVersion` wrapper specifically for the Python bindings to work around https://github.com/PyO3/pyo3/pull/2786 * Added `VersionSpecifiers::contains` * Added `Version::from_release`, a constructor for a version that is just a release such as `3.8`. ## 0.2.0 * Added `VersionSpecifiers`, a thin wrapper around `Vec` with a serde implementation. `VersionSpecifiers::from_str` is now preferred over `parse_version_specifiers`. * Reexport rust function for python modulepep440_rs-0.3.12/License-Apache000064400000000000000000000236751046102023000141300ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS pep440_rs-0.3.12/License-BSD000064400000000000000000000024151046102023000133440ustar 00000000000000Copyright (c) 2023 konstin Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pep440_rs-0.3.12/Readme.md000064400000000000000000000056451046102023000131600ustar 00000000000000# PEP440 in rust [![Crates.io](https://img.shields.io/crates/v/pep440_rs.svg?logo=rust&style=flat-square)](https://crates.io/crates/pep440_rs) [![PyPI](https://img.shields.io/pypi/v/pep440_rs.svg?logo=python&style=flat-square)](https://pypi.org/project/pep440_rs) A library for python version numbers and specifiers, implementing [PEP 440](https://peps.python.org/pep-0440). See [Reimplementing PEP 440](https://cohost.org/konstin/post/514863-reimplementing-pep-4) for some background. Higher level bindings to the requirements syntax are available in [pep508_rs](https://github.com/konstin/pep508_rs). ```rust use std::str::FromStr; use pep440_rs::{parse_version_specifiers, Version, VersionSpecifier}; let version = Version::from_str("1.19").unwrap(); let version_specifier = VersionSpecifier::from_str("==1.*").unwrap(); assert!(version_specifier.contains(&version)); let version_specifiers = parse_version_specifiers(">=1.16, <2.0").unwrap(); assert!(version_specifiers.iter().all(|specifier| specifier.contains(&version))); ``` In python (`pip install pep440_rs`): ```python from pep440_rs import Version, VersionSpecifier assert Version("1.1a1").any_prerelease() assert Version("1.1.dev2").any_prerelease() assert not Version("1.1").any_prerelease() assert VersionSpecifier(">=1.0").contains(Version("1.1a1")) assert not VersionSpecifier(">=1.1").contains(Version("1.1a1")) # Note that python comparisons are the version ordering, not the version specifiers operators assert Version("1.1") >= Version("1.1a1") assert Version("2.0") in VersionSpecifier("==2") ``` PEP 440 has a lot of unintuitive features, including: * An epoch that you can prefix the version which, e.g. `1!1.2.3`. Lower epoch always means lower version (`1.0 <=2!0.1`) * post versions, which can be attached to both stable releases and prereleases * dev versions, which can be attached to sbpth table releases and prereleases. When attached to a prerelease the dev version is ordered just below the normal prerelease, however when attached to a stable version, the dev version is sorted before a prereleases * prerelease handling is a mess: "Pre-releases of any kind, including developmental releases, are implicitly excluded from all version specifiers, unless they are already present on the system, explicitly requested by the user, or if the only available version that satisfies the version specifier is a pre-release.". This means that we can't say whether a specifier matches without also looking at the environment * prelease vs. prerelease incl. dev is fuzzy * local versions on top of all the others, which are added with a + and have implicitly typed string and number segments * no semver-caret (`^`), but a pseudo-semver tilde (`~=`) * ordering contradicts matching: We have e.g. `1.0+local > 1.0` when sorting, but `==1.0` matches `1.0+local`. While the ordering of versions itself is a total order the version matching needs to catch all sorts of special cases pep440_rs-0.3.12/pyproject.toml000064400000000000000000000004721046102023000143460ustar 00000000000000[project] name = "pep440_rs" readme = "python/Readme.md" [build-system] requires = ["maturin>=1.0.0,<2.0.0"] build-backend = "maturin" [tool.maturin] features = ["pyo3"] python-source = "python" module-name = "pep440_rs._pep440_rs" [tool.ruff.per-file-ignores] "python/pep440_rs/__init__.py" = ["F403", "F405"] pep440_rs-0.3.12/python/Readme.md000064400000000000000000000040711046102023000144710ustar 00000000000000# PEP440 in rust A library for python version numbers and specifiers, implementing [PEP 440](https://peps.python.org/pep-0440) ```shell pip install pep440_rs ``` ```python from pep440_rs import Version, VersionSpecifier assert Version("1.1a1").any_prerelease() assert Version("1.1.dev2").any_prerelease() assert not Version("1.1").any_prerelease() assert VersionSpecifier(">=1.0").contains(Version("1.1a1")) assert not VersionSpecifier(">=1.1").contains(Version("1.1a1")) assert Version("2.0") in VersionSpecifier("==2") ``` Unlike [pypa/packaging](https://github.com/pypa/packaging), this library always matches preleases. To only match final releases, filter with `.any_prelease()` beforehand. PEP 440 has a lot of unintuitive features, including: * An epoch that you can prefix the version which, e.g. `1!1.2.3`. Lower epoch always means lower version (`1.0 <=2!0.1`) * post versions, which can be attached to both stable releases and prereleases * dev versions, which can be attached to sbpth table releases and prereleases. When attached to a prerelease the dev version is ordered just below the normal prerelease, however when attached to a stable version, the dev version is sorted before a prereleases * prerelease handling is a mess: "Pre-releases of any kind, including developmental releases, are implicitly excluded from all version specifiers, unless they are already present on the system, explicitly requested by the user, or if the only available version that satisfies the version specifier is a pre-release.". This means that we can't say whether a specifier matches without also looking at the environment * prelease vs. prerelease incl. dev is fuzzy * local versions on top of all the others, which are added with a + and have implicitly typed string and number segments * no semver-caret (`^`), but a pseudo-semver tilde (`~=`) * ordering contradicts matching: We have e.g. `1.0+local > 1.0` when sorting, but `==1.0` matches `1.0+local`. While the ordering of versions itself is a total order the version matching needs to catch all sorts of special cases pep440_rs-0.3.12/src/lib.rs000064400000000000000000000077131046102023000133420ustar 00000000000000//! A library for python version numbers and specifiers, implementing //! [PEP 440](https://peps.python.org/pep-0440) //! //! ```rust //! use std::str::FromStr; //! use pep440_rs::{VersionSpecifiers, Version, VersionSpecifier}; //! //! let version = Version::from_str("1.19").unwrap(); //! let version_specifier = VersionSpecifier::from_str("== 1.*").unwrap(); //! assert!(version_specifier.contains(&version)); //! let version_specifiers = VersionSpecifiers::from_str(">=1.16, <2.0").unwrap(); //! assert!(version_specifiers.iter().all(|specifier| specifier.contains(&version))); //! ``` //! //! One thing that's a bit awkward about the API is that there's two kinds of //! [Version]: One that doesn't allow stars (i.e. a package version), and one that does //! (i.e. a version in a specifier), but they both use the same struct. //! //! The error handling and diagnostics is a bit overdone because this my parser-and-diagnostics //! learning project (which kinda failed because the byte based regex crate and char-based //! diagnostics don't mix well) //! //! PEP 440 has a lot of unintuitive features, including: //! //! * An epoch that you can prefix the version which, e.g. `1!1.2.3`. Lower epoch always means lower //! version (`1.0 <=2!0.1`) //! * post versions, which can be attached to both stable releases and prereleases //! * dev versions, which can be attached to sbpth table releases and prereleases. When attached to a //! prerelease the dev version is ordered just below the normal prerelease, however when attached //! to a stable version, the dev version is sorted before a prereleases //! * prerelease handling is a mess: "Pre-releases of any kind, including developmental releases, //! are implicitly excluded from all version specifiers, unless they are already present on the //! system, explicitly requested by the user, or if the only available version that satisfies //! the version specifier is a pre-release.". This means that we can't say whether a specifier //! matches without also looking at the environment //! * prelease vs. prerelease incl. dev is fuzzy //! * local versions on top of all the others, which are added with a + and have implicitly typed //! string and number segments //! * no semver-caret (`^`), but a pseudo-semver tilde (`~=`) //! * ordering contradicts matching: We have e.g. `1.0+local > 1.0` when sorting, //! but `==1.0` matches `1.0+local`. While the ordering of versions itself is a total order //! the version matching needs to catch all sorts of special cases #![deny(missing_docs)] #[cfg(feature = "pyo3")] use pyo3::{pymodule, types::PyModule, PyResult, Python}; use std::error::Error; use std::fmt::{Display, Formatter}; #[cfg(feature = "pyo3")] pub use version::PyVersion; pub use version::{LocalSegment, Operator, PreRelease, Version}; pub use version_specifier::{parse_version_specifiers, VersionSpecifier, VersionSpecifiers}; mod version; mod version_specifier; /// Error with span information (unicode width) inside the parsed line #[derive(Debug, Eq, PartialEq, Clone)] pub struct Pep440Error { /// The actual error message pub message: String, /// The string that failed to parse pub line: String, /// First character for underlining (unicode width) pub start: usize, /// Number of characters to underline (unicode width) pub width: usize, } impl Display for Pep440Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "Failed to parse version:")?; writeln!(f, "{}", self.line)?; writeln!(f, "{}{}", " ".repeat(self.start), "^".repeat(self.width))?; Ok(()) } } impl Error for Pep440Error {} /// Python bindings shipped as `pep440_rs` #[cfg(feature = "pyo3")] #[pymodule] #[pyo3(name = "_pep440_rs")] pub fn python_module(_py: Python, module: &PyModule) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; module.add_class::()?; Ok(()) } pep440_rs-0.3.12/src/version.rs000064400000000000000000001162501046102023000142560ustar 00000000000000use lazy_static::lazy_static; #[cfg(feature = "pyo3")] use pyo3::{ basic::CompareOp, exceptions::PyValueError, pyclass, pymethods, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, }; use regex::Captures; use regex::Regex; #[cfg(feature = "serde")] use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::cmp::Ordering; #[cfg(feature = "pyo3")] use std::collections::hash_map::DefaultHasher; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; use std::iter; use std::str::FromStr; #[cfg(feature = "tracing")] use tracing::warn; /// A regex copied from , /// updated to support stars for version ranges pub(crate) const VERSION_RE_INNER: &str = r" (?: (?:v?) # (?:(?P[0-9]+)!)? # epoch (?P[0-9*]+(?:\.[0-9]+)*) # release segment, this now allows for * versions which are more lenient than necessary so we can put better error messages in the code (?P # pre-release [-_\.]? (?P(a|b|c|rc|alpha|beta|pre|preview)) [-_\.]? (?P
[0-9]+)?
    )?
    (?P                                   # post release
        (?:-(?P[0-9]+))
        |
        (?:
            [-_\.]?
            (?Ppost|rev|r)
            [-_\.]?
            (?P[0-9]+)?
        )
    )?
    (?P                                    # dev release
        [-_\.]?
        (?Pdev)
        [-_\.]?
        (?P[0-9]+)?
    )?
)
(?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
(?P\.\*)?                          # allow for version matching `.*`
";

lazy_static! {
    /// Matches a python version, such as `1.19.a1`. Based on the PEP 440 regex
    static ref VERSION_RE: Regex = Regex::new(&format!(
        r#"(?xi)^(?:\s*){}(?:\s*)$"#, VERSION_RE_INNER
    )).unwrap();
}

/// One of `~=` `==` `!=` `<=` `>=` `<` `>` `===`
#[derive(Eq, PartialEq, Debug, Hash, Clone)]
#[cfg_attr(feature = "pyo3", pyclass)]
pub enum Operator {
    /// `== 1.2.3`
    Equal,
    /// `== 1.2.*`
    EqualStar,
    /// `===` (discouraged)
    ///
    /// 
    ///
    /// "Use of this operator is heavily discouraged and tooling MAY display a warning when it is used"
    // clippy doesn't like this: #[deprecated = "Use of this operator is heavily discouraged"]
    ExactEqual,
    /// `!= 1.2.3`
    NotEqual,
    /// `!= 1.2.*`
    NotEqualStar,
    /// `~=`
    TildeEqual,
    /// `<`
    LessThan,
    /// `<=`
    LessThanEqual,
    /// `>`
    GreaterThan,
    /// `>=`
    GreaterThanEqual,
}

impl FromStr for Operator {
    type Err = String;

    /// Notably, this does not know about star versions, it just assumes the base operator
    fn from_str(s: &str) -> Result {
        let operator = match s {
            "==" => Self::Equal,
            "===" => {
                #[cfg(feature = "tracing")]
                {
                    warn!("Using arbitrary equality (`===`) is discouraged");
                }
                #[allow(deprecated)]
                Self::ExactEqual
            }
            "!=" => Self::NotEqual,
            "~=" => Self::TildeEqual,
            "<" => Self::LessThan,
            "<=" => Self::LessThanEqual,
            ">" => Self::GreaterThan,
            ">=" => Self::GreaterThanEqual,
            // Should be forbidden by the regex if called from normal parsing
            other => {
                return Err(format!(
                    "No such comparison operator '{}', must be one of ~= == != <= >= < > ===",
                    other
                ));
            }
        };
        Ok(operator)
    }
}

impl Display for Operator {
    /// Note the EqualStar is also `==`
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let operator = match self {
            Operator::Equal => "==",
            // Beware, this doesn't print the star
            Operator::EqualStar => "==",
            #[allow(deprecated)]
            Operator::ExactEqual => "===",
            Operator::NotEqual => "!=",
            Operator::NotEqualStar => "!=",
            Operator::TildeEqual => "~=",
            Operator::LessThan => "<",
            Operator::LessThanEqual => "<=",
            Operator::GreaterThan => ">",
            Operator::GreaterThanEqual => ">=",
        };

        write!(f, "{}", operator)
    }
}

#[cfg(feature = "pyo3")]
#[pymethods]
impl Operator {
    fn __str__(&self) -> String {
        self.to_string()
    }

    fn __repr__(&self) -> String {
        self.to_string()
    }
}

/// Optional prerelease modifier (alpha, beta or release candidate) appended to version
///
/// 
#[derive(PartialEq, Eq, Debug, Hash, Clone, Ord, PartialOrd)]
#[cfg_attr(feature = "pyo3", pyclass)]
pub enum PreRelease {
    /// alpha prerelease
    Alpha,
    /// beta prerelease
    Beta,
    /// release candidate prerelease
    Rc,
}

impl FromStr for PreRelease {
    type Err = String;

    fn from_str(prerelease: &str) -> Result {
        match prerelease.to_lowercase().as_str() {
            "a" | "alpha" => Ok(Self::Alpha),
            "b" | "beta" => Ok(Self::Beta),
            "c" | "rc" | "pre" | "preview" => Ok(Self::Rc),
            _ => Err(format!(
                "'{}' isn't recognized as alpha, beta or release candidate",
                prerelease
            )),
        }
    }
}

impl Display for PreRelease {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Alpha => write!(f, "a"),
            Self::Beta => write!(f, "b"),
            Self::Rc => write!(f, "rc"),
        }
    }
}

/// A part of the [local version identifier]()
///
/// Local versions are a mess:
///
/// > Comparison and ordering of local versions considers each segment of the local version
/// > (divided by a .) separately. If a segment consists entirely of ASCII digits then that section
/// > should be considered an integer for comparison purposes and if a segment contains any ASCII
/// > letters then that segment is compared lexicographically with case insensitivity. When
/// > comparing a numeric and lexicographic segment, the numeric section always compares as greater
/// > than the lexicographic segment. Additionally a local version with a great number of segments
/// > will always compare as greater than a local version with fewer segments, as long as the
/// > shorter local version’s segments match the beginning of the longer local version’s segments
/// > exactly.
///
/// Luckily the default Ord impl for Vec matches the PEP 440 rules
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub enum LocalSegment {
    /// Not-parseable as integer segment of local version
    String(String),
    /// Inferred integer segment of local version
    Number(usize),
}

impl Display for LocalSegment {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::String(string) => write!(f, "{}", string),
            Self::Number(number) => write!(f, "{}", number),
        }
    }
}

impl PartialOrd for LocalSegment {
    fn partial_cmp(&self, other: &Self) -> Option {
        Some(self.cmp(other))
    }
}

impl FromStr for LocalSegment {
    /// This can be a never type when stabilized
    type Err = ();

    fn from_str(segment: &str) -> Result {
        Ok(if let Ok(number) = segment.parse::() {
            Self::Number(number)
        } else {
            // "and if a segment contains any ASCII letters then that segment is compared lexicographically with case insensitivity"
            Self::String(segment.to_lowercase())
        })
    }
}

/// A version number such as `1.2.3` or `4!5.6.7-a8.post9.dev0`.
///
/// Beware that the sorting implemented with [Ord] and [Eq] is not consistent with the operators
/// from PEP 440, i.e. compare two versions in rust with `>` gives a different result than a
/// VersionSpecifier with `>` as operator.
///
/// Parse with [Version::from_str]:
///
/// ```rust
/// use std::str::FromStr;
/// use pep440_rs::Version;
///
/// let version = Version::from_str("1.19").unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Version {
    /// The [versioning epoch](https://peps.python.org/pep-0440/#version-epochs). Normally just 0,
    /// but you can increment it if you switched the versioning scheme.
    pub epoch: usize,
    /// The normal number part of the version
    /// (["final release"](https://peps.python.org/pep-0440/#final-releases)),
    /// such a `1.2.3` in `4!1.2.3-a8.post9.dev1`
    ///
    /// Note that we drop the * placeholder by moving it to `Operator`
    pub release: Vec,
    /// The [prerelease](https://peps.python.org/pep-0440/#pre-releases), i.e. alpha, beta or rc
    /// plus a number
    ///
    /// Note that whether this is Some influences the version
    /// range matching since normally we exclude all prerelease versions
    pub pre: Option<(PreRelease, usize)>,
    /// The [Post release version](https://peps.python.org/pep-0440/#post-releases),
    /// higher post version are preferred over lower post or none-post versions
    pub post: Option,
    /// The [developmental release](https://peps.python.org/pep-0440/#developmental-releases),
    /// if any
    pub dev: Option,
    /// A [local version identifier](https://peps.python.org/pep-0440/#local-version-identifiers)
    /// such as `+deadbeef` in `1.2.3+deadbeef`
    ///
    /// > They consist of a normal public version identifier (as defined in the previous section),
    /// > along with an arbitrary “local version label”, separated from the public version
    /// > identifier by a plus. Local version labels have no specific semantics assigned, but some
    /// > syntactic restrictions are imposed.
    pub local: Option>,
}

#[cfg(feature = "pyo3")]
impl IntoPy for Version {
    fn into_py(self, py: Python<'_>) -> PyObject {
        PyVersion(self).into_py(py)
    }
}

#[cfg(feature = "pyo3")]
impl<'source> FromPyObject<'source> for Version {
    fn extract(ob: &'source PyAny) -> PyResult {
        Ok(ob.extract::()?.0)
    }
}

/// Workaround for https://github.com/PyO3/pyo3/pull/2786
#[cfg(feature = "pyo3")]
#[derive(Clone, Debug)]
#[pyclass(name = "Version")]
pub struct PyVersion(pub Version);

#[cfg(feature = "pyo3")]
#[pymethods]
impl PyVersion {
    /// The [versioning epoch](https://peps.python.org/pep-0440/#version-epochs). Normally just 0,
    /// but you can increment it if you switched the versioning scheme.
    #[getter]
    pub fn epoch(&self) -> usize {
        self.0.epoch
    }
    /// The normal number part of the version
    /// (["final release"](https://peps.python.org/pep-0440/#final-releases)),
    /// such a `1.2.3` in `4!1.2.3-a8.post9.dev1`
    ///
    /// Note that we drop the * placeholder by moving it to `Operator`
    #[getter]
    pub fn release(&self) -> Vec {
        self.0.release.clone()
    }
    /// The [prerelease](https://peps.python.org/pep-0440/#pre-releases), i.e. alpha, beta or rc
    /// plus a number
    ///
    /// Note that whether this is Some influences the version
    /// range matching since normally we exclude all prerelease versions
    #[getter]
    pub fn pre(&self) -> Option<(PreRelease, usize)> {
        self.0.pre.clone()
    }
    /// The [Post release version](https://peps.python.org/pep-0440/#post-releases),
    /// higher post version are preferred over lower post or none-post versions
    #[getter]
    pub fn post(&self) -> Option {
        self.0.post
    }
    /// The [developmental release](https://peps.python.org/pep-0440/#developmental-releases),
    /// if any
    #[getter]
    pub fn dev(&self) -> Option {
        self.0.dev
    }
    /// The first item of release or 0 if unavailable.
    #[getter]
    #[allow(clippy::get_first)]
    pub fn major(&self) -> usize {
        self.release().get(0).cloned().unwrap_or_default()
    }
    /// The second item of release or 0 if unavailable.
    #[getter]
    pub fn minor(&self) -> usize {
        self.release().get(1).cloned().unwrap_or_default()
    }
    /// The third item of release or 0 if unavailable.
    #[getter]
    pub fn micro(&self) -> usize {
        self.release().get(2).cloned().unwrap_or_default()
    }

    /// Parses a PEP 440 version string
    #[cfg(feature = "pyo3")]
    #[new]
    pub fn parse(version: String) -> PyResult {
        Ok(Self(
            Version::from_str(&version).map_err(PyValueError::new_err)?,
        ))
    }

    // Maps the error type
    /// Parse a PEP 440 version optionally ending with `.*`
    #[cfg(feature = "pyo3")]
    #[staticmethod]
    pub fn parse_star(version_specifier: String) -> PyResult<(Self, bool)> {
        Version::from_str_star(&version_specifier)
            .map_err(PyValueError::new_err)
            .map(|(version, star)| (Self(version), star))
    }

    /// Returns the normalized representation
    #[cfg(feature = "pyo3")]
    pub fn __str__(&self) -> String {
        self.0.to_string()
    }

    /// Returns the normalized representation
    #[cfg(feature = "pyo3")]
    pub fn __repr__(&self) -> String {
        format!(r#""#, self.0)
    }

    /// Returns the normalized representation
    #[cfg(feature = "pyo3")]
    pub fn __hash__(&self) -> u64 {
        let mut hasher = DefaultHasher::new();
        self.0.hash(&mut hasher);
        hasher.finish()
    }

    #[cfg(feature = "pyo3")]
    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
        op.matches(self.0.cmp(&other.0))
    }

    fn any_prerelease(&self) -> bool {
        self.0.any_prerelease()
    }
}

/// https://github.com/serde-rs/serde/issues/1316#issue-332908452
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Version {
    fn deserialize(deserializer: D) -> Result
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        FromStr::from_str(&s).map_err(de::Error::custom)
    }
}

/// https://github.com/serde-rs/serde/issues/1316#issue-332908452
#[cfg(feature = "serde")]
impl Serialize for Version {
    fn serialize(&self, serializer: S) -> Result
    where
        S: Serializer,
    {
        serializer.collect_str(self)
    }
}

impl Version {
    /// Constructor for a version that is just a release such as `3.8`
    pub fn from_release(release: Vec) -> Self {
        Self {
            epoch: 0,
            release,
            pre: None,
            post: None,
            dev: None,
            local: None,
        }
    }

    /// For PEP 440 specifier matching: "Except where specifically noted below, local version
    /// identifiers MUST NOT be permitted in version specifiers, and local version labels MUST be
    /// ignored entirely when checking if candidate versions match a given version specifier."
    pub(crate) fn without_local(&self) -> Self {
        Self {
            local: None,
            ..self.clone()
        }
    }
}

impl Version {
    /// Whether this is an alpha/beta/rc or dev version
    pub fn any_prerelease(&self) -> bool {
        self.is_pre() || self.is_dev()
    }

    /// Whether this is an alpha/beta/rc version
    pub fn is_pre(&self) -> bool {
        self.pre.is_some()
    }

    /// Whether this is a dev version
    pub fn is_dev(&self) -> bool {
        self.dev.is_some()
    }

    /// Whether this is a post version
    pub fn is_post(&self) -> bool {
        self.post.is_some()
    }

    /// Whether this is a local version (e.g. `1.2.3+localsuffixesareweird`)
    pub fn is_local(&self) -> bool {
        self.local.is_some()
    }
}

/// Shows normalized version
impl Display for Version {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let epoch = if self.epoch == 0 {
            "".to_string()
        } else {
            format!("{}!", self.epoch)
        };
        let release = self
            .release
            .iter()
            .map(|x| x.to_string())
            .collect::>()
            .join(".");
        let pre = self
            .pre
            .as_ref()
            .map(|(pre_kind, pre_version)| format!("{}{}", pre_kind, pre_version))
            .unwrap_or_default();
        let post = self
            .post
            .map(|post| format!(".post{}", post))
            .unwrap_or_default();
        let dev = self
            .dev
            .map(|dev| format!(".dev{}", dev))
            .unwrap_or_default();
        let local = self
            .local
            .as_ref()
            .map(|segments| {
                format!(
                    "+{}",
                    segments
                        .iter()
                        .map(|x| x.to_string())
                        .collect::>()
                        .join(".")
                )
            })
            .unwrap_or_default();
        write!(f, "{}{}{}{}{}{}", epoch, release, pre, post, dev, local)
    }
}

/// Compare the release parts of two versions, e.g. `4.3.1` > `4.2`, `1.1.0` == `1.1` and
/// `1.16` < `1.19`
pub fn compare_release(this: &[usize], other: &[usize]) -> Ordering {
    // "When comparing release segments with different numbers of components, the shorter segment
    // is padded out with additional zeros as necessary"
    let iterator: Vec<(&usize, &usize)> = if this.len() < other.len() {
        this.iter().chain(iter::repeat(&0)).zip(other).collect()
    } else {
        this.iter()
            .zip(other.iter().chain(iter::repeat(&0)))
            .collect()
    };

    for (a, b) in iterator {
        if a != b {
            return a.cmp(b);
        }
    }

    Ordering::Equal
}

/// Compare the parts attached after the release, given equal release
///
/// According to 
/// the order of pre/post-releases is:
/// .devN, aN, bN, rcN, , .postN
/// but also, you can have dev/post releases on beta releases, so we make a three stage ordering:
/// ({dev: 0, a: 1, b: 2, rc: 3, (): 4, post: 5}, , , , )
///
/// For post, any number is better than none (so None defaults to None<0), but for dev, no number
/// is better (so None default to the maximum). For local the Option> luckily already has the
/// correct default Ord implementation
fn sortable_tuple(
    version: &Version,
) -> (
    usize,
    usize,
    Option,
    usize,
    Option>,
) {
    match (&version.pre, &version.post, &version.dev) {
        // dev release
        (None, None, Some(n)) => (0, 0, None, *n, version.local.clone()),
        // alpha release
        (Some((PreRelease::Alpha, n)), post, dev) => (
            1,
            *n,
            *post,
            dev.unwrap_or(usize::MAX),
            version.local.clone(),
        ),
        // beta release
        (Some((PreRelease::Beta, n)), post, dev) => (
            2,
            *n,
            *post,
            dev.unwrap_or(usize::MAX),
            version.local.clone(),
        ),
        // alpha release
        (Some((PreRelease::Rc, n)), post, dev) => (
            3,
            *n,
            *post,
            dev.unwrap_or(usize::MAX),
            version.local.clone(),
        ),
        // final release
        (None, None, None) => (4, 0, None, 0, version.local.clone()),
        // post release
        (None, Some(post), dev) => (
            5,
            0,
            Some(*post),
            dev.unwrap_or(usize::MAX),
            version.local.clone(),
        ),
    }
}

impl PartialEq for Version {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

impl Eq for Version {}

impl Hash for Version {
    /// Custom implementation to ignoring trailing zero because PartialEq zero pads
    fn hash(&self, state: &mut H) {
        self.epoch.hash(state);
        // Skip trailing zeros
        for i in self.release.iter().rev().skip_while(|x| **x == 0) {
            i.hash(state);
        }
        self.pre.hash(state);
        self.dev.hash(state);
        self.post.hash(state);
        self.local.hash(state);
    }
}

impl PartialOrd for Version {
    fn partial_cmp(&self, other: &Self) -> Option {
        Some(self.cmp(other))
    }
}

impl Ord for Version {
    /// 1.0.dev456 < 1.0a1 < 1.0a2.dev456 < 1.0a12.dev456 < 1.0a12 < 1.0b1.dev456 < 1.0b2
    /// < 1.0b2.post345.dev456 < 1.0b2.post345 < 1.0b2-346 < 1.0c1.dev456 < 1.0c1 < 1.0rc2 < 1.0c3
    /// < 1.0 < 1.0.post456.dev34 < 1.0.post456
    fn cmp(&self, other: &Self) -> Ordering {
        if self.epoch != other.epoch {
            return self.epoch.cmp(&other.epoch);
        }

        match compare_release(&self.release, &other.release) {
            Ordering::Less => {
                return Ordering::Less;
            }
            Ordering::Equal => {}
            Ordering::Greater => {
                return Ordering::Greater;
            }
        }
        // release is equal, so compare the other parts
        sortable_tuple(self).cmp(&sortable_tuple(other))
    }
}

impl Ord for LocalSegment {
    fn cmp(&self, other: &Self) -> Ordering {
        // 
        match (self, other) {
            (Self::Number(n1), Self::Number(n2)) => n1.cmp(n2),
            (Self::String(s1), Self::String(s2)) => s1.cmp(s2),
            (Self::Number(_), Self::String(_)) => Ordering::Greater,
            (Self::String(_), Self::Number(_)) => Ordering::Less,
        }
    }
}

impl FromStr for Version {
    type Err = String;

    /// Parses a version such as `1.19`, `1.0a1`,`1.0+abc.5` or `1!2012.2`
    ///
    /// Note that this variant doesn't allow the version to end with a star, see
    /// [Self::from_str_star] if you want to parse versions for specifiers
    fn from_str(version: &str) -> Result {
        let captures = VERSION_RE
            .captures(version)
            .ok_or_else(|| format!("Version `{}` doesn't match PEP 440 rules", version))?;
        let (version, star) = Version::parse_impl(&captures)?;
        if star {
            return Err("A star (`*`) must not be used in a fixed version (use `Version::from_string_star` otherwise)".to_string());
        }
        Ok(version)
    }
}

impl Version {
    /// Like [Self::from_str], but also allows the version to end with a star and returns whether it
    /// did. This variant is for use in specifiers.
    ///  * `1.2.3` -> false
    ///  * `1.2.3.*` -> true
    ///  * `1.2.*.4` -> err
    ///  * `1.0-dev1.*` -> err
    pub fn from_str_star(version: &str) -> Result<(Self, bool), String> {
        let captures = VERSION_RE
            .captures(version)
            .ok_or_else(|| format!("Version `{}` doesn't match PEP 440 rules", version))?;
        let (version, star) = Version::parse_impl(&captures)?;
        Ok((version, star))
    }

    /// Extracted for reusability around star/non-star
    pub(crate) fn parse_impl(captures: &Captures) -> Result<(Version, bool), String> {
        let number_field = |field_name| {
            if let Some(field_str) = captures.name(field_name) {
                match field_str.as_str().parse::() {
                    Ok(number) => Ok(Some(number)),
                    // Should be already forbidden by the regex
                    Err(err) => Err(format!(
                        "Couldn't parse '{}' as number from {}: {}",
                        field_str.as_str(),
                        field_name,
                        err
                    )),
                }
            } else {
                Ok(None)
            }
        };
        let epoch = number_field("epoch")?
            // "If no explicit epoch is given, the implicit epoch is 0"
            .unwrap_or_default();
        let pre = {
            let pre_type = captures
                .name("pre_name")
                .map(|pre| PreRelease::from_str(pre.as_str()))
                // Shouldn't fail due to the regex
                .transpose()?;
            let pre_number = number_field("pre")?
                // 
                .unwrap_or_default();
            pre_type.map(|pre_type| (pre_type, pre_number))
        };
        let post = if captures.name("post_field").is_some() {
            // While PEP 440 says .post is "followed by a non-negative integer value",
            // packaging has tests that ensure that it defaults to 0
            // https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L187-L202
            Some(
                number_field("post_new")?
                    .or(number_field("post_old")?)
                    .unwrap_or_default(),
            )
        } else {
            None
        };
        let dev = if captures.name("dev_field").is_some() {
            // 
            Some(number_field("dev")?.unwrap_or_default())
        } else {
            None
        };
        let local = captures.name("local").map(|local| {
            local
                .as_str()
                .split(&['-', '_', '.'][..])
                .map(|segment| {
                    if let Ok(number) = segment.parse::() {
                        LocalSegment::Number(number)
                    } else {
                        // "and if a segment contains any ASCII letters then that segment is compared lexicographically with case insensitivity"
                        LocalSegment::String(segment.to_lowercase())
                    }
                })
                .collect()
        });
        let release = captures
            .name("release")
            // Should be forbidden by the regex
            .ok_or_else(|| "No release in version".to_string())?
            .as_str()
            .split('.')
            .map(|segment| segment.parse::().map_err(|err| err.to_string()))
            .collect::, String>>()?;

        let star = captures.name("trailing_dot_star").is_some();
        if star {
            if pre.is_some() {
                return Err(
                    "You can't have both a trailing `.*` and a prerelease version".to_string(),
                );
            }
            if post.is_some() {
                return Err("You can't have both a trailing `.*` and a post version".to_string());
            }
            if dev.is_some() {
                return Err("You can't have both a trailing `.*` and a dev version".to_string());
            }
            if local.is_some() {
                return Err("You can't have both a trailing `.*` and a local version".to_string());
            }
        }

        let version = Version {
            epoch,
            release,
            pre,
            post,
            dev,
            local,
        };
        Ok((version, star))
    }
}

#[cfg(test)]
mod test {
    #[cfg(feature = "pyo3")]
    use pyo3::pyfunction;
    use std::str::FromStr;

    use crate::{Version, VersionSpecifier};

    /// 
    #[test]
    fn test_packaging_versions() {
        let versions = [
            // Implicit epoch of 0
            "1.0.dev456",
            "1.0a1",
            "1.0a2.dev456",
            "1.0a12.dev456",
            "1.0a12",
            "1.0b1.dev456",
            "1.0b2",
            "1.0b2.post345.dev456",
            "1.0b2.post345",
            "1.0b2-346",
            "1.0c1.dev456",
            "1.0c1",
            "1.0rc2",
            "1.0c3",
            "1.0",
            "1.0.post456.dev34",
            "1.0.post456",
            "1.1.dev1",
            "1.2+123abc",
            "1.2+123abc456",
            "1.2+abc",
            "1.2+abc123",
            "1.2+abc123def",
            "1.2+1234.abc",
            "1.2+123456",
            "1.2.r32+123456",
            "1.2.rev33+123456",
            // Explicit epoch of 1
            "1!1.0.dev456",
            "1!1.0a1",
            "1!1.0a2.dev456",
            "1!1.0a12.dev456",
            "1!1.0a12",
            "1!1.0b1.dev456",
            "1!1.0b2",
            "1!1.0b2.post345.dev456",
            "1!1.0b2.post345",
            "1!1.0b2-346",
            "1!1.0c1.dev456",
            "1!1.0c1",
            "1!1.0rc2",
            "1!1.0c3",
            "1!1.0",
            "1!1.0.post456.dev34",
            "1!1.0.post456",
            "1!1.1.dev1",
            "1!1.2+123abc",
            "1!1.2+123abc456",
            "1!1.2+abc",
            "1!1.2+abc123",
            "1!1.2+abc123def",
            "1!1.2+1234.abc",
            "1!1.2+123456",
            "1!1.2.r32+123456",
            "1!1.2.rev33+123456",
        ];
        for version in versions {
            Version::from_str(version).unwrap();
            VersionSpecifier::from_str(&format!("=={}", version)).unwrap();
        }
    }

    /// 
    #[test]
    fn test_packaging_failures() {
        let versions = [
            // Nonsensical versions should be invalid
            "french toast",
            // Versions with invalid local versions
            "1.0+a+",
            "1.0++",
            "1.0+_foobar",
            "1.0+foo&asd",
            "1.0+1+1",
        ];
        for version in versions {
            assert_eq!(
                Version::from_str(version).unwrap_err(),
                format!("Version `{}` doesn't match PEP 440 rules", version)
            );
            assert_eq!(
                VersionSpecifier::from_str(&format!("=={}", version)).unwrap_err(),
                format!(
                    "Version specifier `=={}` doesn't match PEP 440 rules",
                    version
                )
            );
        }
    }

    #[test]
    fn test_equality_and_normalization() {
        let versions = [
            // Various development release incarnations
            ("1.0dev", "1.0.dev0"),
            ("1.0.dev", "1.0.dev0"),
            ("1.0dev1", "1.0.dev1"),
            ("1.0dev", "1.0.dev0"),
            ("1.0-dev", "1.0.dev0"),
            ("1.0-dev1", "1.0.dev1"),
            ("1.0DEV", "1.0.dev0"),
            ("1.0.DEV", "1.0.dev0"),
            ("1.0DEV1", "1.0.dev1"),
            ("1.0DEV", "1.0.dev0"),
            ("1.0.DEV1", "1.0.dev1"),
            ("1.0-DEV", "1.0.dev0"),
            ("1.0-DEV1", "1.0.dev1"),
            // Various alpha incarnations
            ("1.0a", "1.0a0"),
            ("1.0.a", "1.0a0"),
            ("1.0.a1", "1.0a1"),
            ("1.0-a", "1.0a0"),
            ("1.0-a1", "1.0a1"),
            ("1.0alpha", "1.0a0"),
            ("1.0.alpha", "1.0a0"),
            ("1.0.alpha1", "1.0a1"),
            ("1.0-alpha", "1.0a0"),
            ("1.0-alpha1", "1.0a1"),
            ("1.0A", "1.0a0"),
            ("1.0.A", "1.0a0"),
            ("1.0.A1", "1.0a1"),
            ("1.0-A", "1.0a0"),
            ("1.0-A1", "1.0a1"),
            ("1.0ALPHA", "1.0a0"),
            ("1.0.ALPHA", "1.0a0"),
            ("1.0.ALPHA1", "1.0a1"),
            ("1.0-ALPHA", "1.0a0"),
            ("1.0-ALPHA1", "1.0a1"),
            // Various beta incarnations
            ("1.0b", "1.0b0"),
            ("1.0.b", "1.0b0"),
            ("1.0.b1", "1.0b1"),
            ("1.0-b", "1.0b0"),
            ("1.0-b1", "1.0b1"),
            ("1.0beta", "1.0b0"),
            ("1.0.beta", "1.0b0"),
            ("1.0.beta1", "1.0b1"),
            ("1.0-beta", "1.0b0"),
            ("1.0-beta1", "1.0b1"),
            ("1.0B", "1.0b0"),
            ("1.0.B", "1.0b0"),
            ("1.0.B1", "1.0b1"),
            ("1.0-B", "1.0b0"),
            ("1.0-B1", "1.0b1"),
            ("1.0BETA", "1.0b0"),
            ("1.0.BETA", "1.0b0"),
            ("1.0.BETA1", "1.0b1"),
            ("1.0-BETA", "1.0b0"),
            ("1.0-BETA1", "1.0b1"),
            // Various release candidate incarnations
            ("1.0c", "1.0rc0"),
            ("1.0.c", "1.0rc0"),
            ("1.0.c1", "1.0rc1"),
            ("1.0-c", "1.0rc0"),
            ("1.0-c1", "1.0rc1"),
            ("1.0rc", "1.0rc0"),
            ("1.0.rc", "1.0rc0"),
            ("1.0.rc1", "1.0rc1"),
            ("1.0-rc", "1.0rc0"),
            ("1.0-rc1", "1.0rc1"),
            ("1.0C", "1.0rc0"),
            ("1.0.C", "1.0rc0"),
            ("1.0.C1", "1.0rc1"),
            ("1.0-C", "1.0rc0"),
            ("1.0-C1", "1.0rc1"),
            ("1.0RC", "1.0rc0"),
            ("1.0.RC", "1.0rc0"),
            ("1.0.RC1", "1.0rc1"),
            ("1.0-RC", "1.0rc0"),
            ("1.0-RC1", "1.0rc1"),
            // Various post release incarnations
            ("1.0post", "1.0.post0"),
            ("1.0.post", "1.0.post0"),
            ("1.0post1", "1.0.post1"),
            ("1.0post", "1.0.post0"),
            ("1.0-post", "1.0.post0"),
            ("1.0-post1", "1.0.post1"),
            ("1.0POST", "1.0.post0"),
            ("1.0.POST", "1.0.post0"),
            ("1.0POST1", "1.0.post1"),
            ("1.0POST", "1.0.post0"),
            ("1.0r", "1.0.post0"),
            ("1.0rev", "1.0.post0"),
            ("1.0.POST1", "1.0.post1"),
            ("1.0.r1", "1.0.post1"),
            ("1.0.rev1", "1.0.post1"),
            ("1.0-POST", "1.0.post0"),
            ("1.0-POST1", "1.0.post1"),
            ("1.0-5", "1.0.post5"),
            ("1.0-r5", "1.0.post5"),
            ("1.0-rev5", "1.0.post5"),
            // Local version case insensitivity
            ("1.0+AbC", "1.0+abc"),
            // Integer Normalization
            ("1.01", "1.1"),
            ("1.0a05", "1.0a5"),
            ("1.0b07", "1.0b7"),
            ("1.0c056", "1.0rc56"),
            ("1.0rc09", "1.0rc9"),
            ("1.0.post000", "1.0.post0"),
            ("1.1.dev09000", "1.1.dev9000"),
            ("00!1.2", "1.2"),
            ("0100!0.0", "100!0.0"),
            // Various other normalizations
            ("v1.0", "1.0"),
            ("   v1.0\t\n", "1.0"),
        ];
        for (version_str, normalized_str) in versions {
            let version = Version::from_str(version_str).unwrap();
            let normalized = Version::from_str(normalized_str).unwrap();
            // Just test version parsing again
            assert_eq!(version, normalized, "{} {}", version_str, normalized_str);
            // Test version normalization
            assert_eq!(
                version.to_string(),
                normalized.to_string(),
                "{} {}",
                version_str,
                normalized_str
            );
        }
    }

    /// https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L229-L277
    #[test]
    fn test_equality_and_normalization2() {
        let versions = [
            ("1.0.dev456", "1.0.dev456"),
            ("1.0a1", "1.0a1"),
            ("1.0a2.dev456", "1.0a2.dev456"),
            ("1.0a12.dev456", "1.0a12.dev456"),
            ("1.0a12", "1.0a12"),
            ("1.0b1.dev456", "1.0b1.dev456"),
            ("1.0b2", "1.0b2"),
            ("1.0b2.post345.dev456", "1.0b2.post345.dev456"),
            ("1.0b2.post345", "1.0b2.post345"),
            ("1.0rc1.dev456", "1.0rc1.dev456"),
            ("1.0rc1", "1.0rc1"),
            ("1.0", "1.0"),
            ("1.0.post456.dev34", "1.0.post456.dev34"),
            ("1.0.post456", "1.0.post456"),
            ("1.0.1", "1.0.1"),
            ("0!1.0.2", "1.0.2"),
            ("1.0.3+7", "1.0.3+7"),
            ("0!1.0.4+8.0", "1.0.4+8.0"),
            ("1.0.5+9.5", "1.0.5+9.5"),
            ("1.2+1234.abc", "1.2+1234.abc"),
            ("1.2+123456", "1.2+123456"),
            ("1.2+123abc", "1.2+123abc"),
            ("1.2+123abc456", "1.2+123abc456"),
            ("1.2+abc", "1.2+abc"),
            ("1.2+abc123", "1.2+abc123"),
            ("1.2+abc123def", "1.2+abc123def"),
            ("1.1.dev1", "1.1.dev1"),
            ("7!1.0.dev456", "7!1.0.dev456"),
            ("7!1.0a1", "7!1.0a1"),
            ("7!1.0a2.dev456", "7!1.0a2.dev456"),
            ("7!1.0a12.dev456", "7!1.0a12.dev456"),
            ("7!1.0a12", "7!1.0a12"),
            ("7!1.0b1.dev456", "7!1.0b1.dev456"),
            ("7!1.0b2", "7!1.0b2"),
            ("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"),
            ("7!1.0b2.post345", "7!1.0b2.post345"),
            ("7!1.0rc1.dev456", "7!1.0rc1.dev456"),
            ("7!1.0rc1", "7!1.0rc1"),
            ("7!1.0", "7!1.0"),
            ("7!1.0.post456.dev34", "7!1.0.post456.dev34"),
            ("7!1.0.post456", "7!1.0.post456"),
            ("7!1.0.1", "7!1.0.1"),
            ("7!1.0.2", "7!1.0.2"),
            ("7!1.0.3+7", "7!1.0.3+7"),
            ("7!1.0.4+8.0", "7!1.0.4+8.0"),
            ("7!1.0.5+9.5", "7!1.0.5+9.5"),
            ("7!1.1.dev1", "7!1.1.dev1"),
        ];
        for (version_str, normalized_str) in versions {
            let version = Version::from_str(version_str).unwrap();
            let normalized = Version::from_str(normalized_str).unwrap();
            assert_eq!(version, normalized, "{} {}", version_str, normalized_str);
            // Test version normalization
            assert_eq!(
                version.to_string(),
                normalized_str,
                "{} {}",
                version_str,
                normalized_str
            );
            // Since we're already at it
            assert_eq!(
                version.to_string(),
                normalized.to_string(),
                "{} {}",
                version_str,
                normalized_str
            );
        }
    }

    #[test]
    fn test_star_fixed_version() {
        let result = Version::from_str("0.9.1.*");
        assert_eq!(
            result.unwrap_err(),
            "A star (`*`) must not be used in a fixed version (use `Version::from_string_star` otherwise)"
        );
    }

    #[test]
    fn test_regex_mismatch() {
        let result = Version::from_str("blergh");
        assert_eq!(
            result.unwrap_err(),
            "Version `blergh` doesn't match PEP 440 rules"
        );
    }

    #[test]
    fn test_from_version_star() {
        assert!(!Version::from_str_star("1.2.3").unwrap().1);
        assert!(Version::from_str_star("1.2.3.*").unwrap().1);
        assert_eq!(
            Version::from_str_star("1.2.*.4.*").unwrap_err(),
            "Version `1.2.*.4.*` doesn't match PEP 440 rules"
        );
        assert_eq!(
            Version::from_str_star("1.0-dev1.*").unwrap_err(),
            "You can't have both a trailing `.*` and a dev version"
        );
        assert_eq!(
            Version::from_str_star("1.0a1.*").unwrap_err(),
            "You can't have both a trailing `.*` and a prerelease version"
        );
        assert_eq!(
            Version::from_str_star("1.0.post1.*").unwrap_err(),
            "You can't have both a trailing `.*` and a post version"
        );
        assert_eq!(
            Version::from_str_star("1.0+lolwat.*").unwrap_err(),
            "You can't have both a trailing `.*` and a local version"
        );
    }

    #[cfg(feature = "pyo3")]
    #[pyfunction]
    fn _convert_in_and_out(version: Version) -> Version {
        version
    }
}
pep440_rs-0.3.12/src/version_specifier.rs000064400000000000000000001232431046102023000163070ustar  00000000000000#[cfg(feature = "pyo3")]
use crate::version::PyVersion;
use crate::version::VERSION_RE_INNER;
use crate::{version, Operator, Pep440Error, Version};
use lazy_static::lazy_static;
#[cfg(feature = "pyo3")]
use pyo3::{
    exceptions::{PyIndexError, PyNotImplementedError, PyValueError},
    pyclass,
    pyclass::CompareOp,
    pymethods, Py, PyRef, PyRefMut, PyResult,
};
use regex::Regex;
#[cfg(feature = "serde")]
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::cmp::Ordering;
#[cfg(feature = "pyo3")]
use std::collections::hash_map::DefaultHasher;
use std::fmt::Formatter;
use std::fmt::{Debug, Display};
#[cfg(feature = "pyo3")]
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::str::FromStr;
use unicode_width::UnicodeWidthStr;

#[cfg(feature = "tracing")]
use tracing::warn;

lazy_static! {
    /// Matches a python version specifier, such as `>=1.19.a1` or `4.1.*`. Extends the PEP 440
    /// version regex to version specifiers
    static ref VERSION_SPECIFIER_RE: Regex = Regex::new(&format!(
        r#"(?xi)^(?:\s*)(?P(~=|==|!=|<=|>=|<|>|===))(?:\s*){}(?:\s*)$"#,
        VERSION_RE_INNER
    )).unwrap();
}

/// A thin wrapper around `Vec` with a serde implementation
///
/// Python requirements can contain multiple version specifier so we need to store them in a list,
/// such as `>1.2,<2.0` being `[">1.2", "<2.0"]`.
///
/// You can use the serde implementation to e.g. parse `requires-python` from pyproject.toml
///
/// ```rust
/// # use std::str::FromStr;
/// # use pep440_rs::{VersionSpecifiers, Version, Operator};
///
/// let version = Version::from_str("1.19").unwrap();
/// let version_specifiers = VersionSpecifiers::from_str(">=1.16, <2.0").unwrap();
/// assert!(version_specifiers.contains(&version));
/// // VersionSpecifiers derefs into a list of specifiers
/// assert_eq!(version_specifiers.iter().position(|specifier| *specifier.operator() == Operator::LessThan), Some(1));
/// ```
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
#[cfg_attr(feature = "pyo3", pyclass(sequence))]
pub struct VersionSpecifiers(Vec);

impl Deref for VersionSpecifiers {
    type Target = [VersionSpecifier];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl VersionSpecifiers {
    /// Whether all specifiers match the given version
    pub fn contains(&self, version: &Version) -> bool {
        self.iter().all(|specifier| specifier.contains(version))
    }
}

impl FromIterator for VersionSpecifiers {
    fn from_iter>(iter: T) -> Self {
        Self(iter.into_iter().collect())
    }
}

impl FromStr for VersionSpecifiers {
    type Err = Pep440Error;

    fn from_str(s: &str) -> Result {
        parse_version_specifiers(s).map(Self)
    }
}

impl Display for VersionSpecifiers {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        for (idx, version_specifier) in self.0.iter().enumerate() {
            // Separate version specifiers by comma, but we need one comma less than there are
            // specifiers
            if idx != 0 {
                write!(f, ", {}", version_specifier)?;
            } else {
                write!(f, "{}", version_specifier)?;
            }
        }
        Ok(())
    }
}

/// https://pyo3.rs/v0.18.2/class/protocols.html#iterable-objects
#[cfg(feature = "pyo3")]
#[pyclass]
struct VersionSpecifiersIter {
    inner: std::vec::IntoIter,
}

#[cfg(feature = "pyo3")]
#[pymethods]
impl VersionSpecifiersIter {
    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    fn __next__(mut slf: PyRefMut<'_, Self>) -> Option {
        slf.inner.next()
    }
}

#[cfg(feature = "pyo3")]
#[pymethods]
impl VersionSpecifiers {
    /// PEP 440 parsing
    #[new]
    pub fn __new__(version_specifiers: String) -> PyResult {
        Self::from_str(&version_specifiers).map_err(|err| PyValueError::new_err(err.to_string()))
    }

    /// PEP 440 serialization
    pub fn __str__(&self) -> String {
        self.to_string()
    }

    /// PEP 440 serialization
    pub fn __repr__(&self) -> String {
        self.to_string()
    }

    /// Get the nth VersionSpecifier
    pub fn __getitem__(&self, idx: usize) -> PyResult {
        self.0.get(idx).cloned().ok_or_else(|| {
            PyIndexError::new_err(format!(
                "list index {} our of range for len {}",
                idx,
                self.0.len()
            ))
        })
    }

    fn __iter__(slf: PyRef<'_, Self>) -> PyResult> {
        let iter = VersionSpecifiersIter {
            inner: slf.0.clone().into_iter(),
        };
        Py::new(slf.py(), iter)
    }

    /// Get the number of VersionSpecifier
    pub fn __len__(&self) -> usize {
        self.0.len()
    }
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for VersionSpecifiers {
    fn deserialize(deserializer: D) -> Result
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Self::from_str(&s).map_err(de::Error::custom)
    }
}

#[cfg(feature = "serde")]
impl Serialize for VersionSpecifiers {
    #[allow(unstable_name_collisions)]
    fn serialize(&self, serializer: S) -> Result
    where
        S: Serializer,
    {
        serializer.collect_str(
            &self
                .iter()
                .map(ToString::to_string)
                .collect::>()
                .join(","),
        )
    }
}

/// A version range such such as `>1.2.3`, `<=4!5.6.7-a8.post9.dev0` or `== 4.1.*`. Parse with
/// `VersionSpecifier::from_str`
///
/// ```rust
/// use std::str::FromStr;
/// use pep440_rs::{Version, VersionSpecifier};
///
/// let version = Version::from_str("1.19").unwrap();
/// let version_specifier = VersionSpecifier::from_str("== 1.*").unwrap();
/// assert!(version_specifier.contains(&version));
/// ```
#[cfg_attr(feature = "pyo3", pyclass(get_all))]
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct VersionSpecifier {
    /// ~=|==|!=|<=|>=|<|>|===, plus whether the version ended with a star
    pub(crate) operator: Operator,
    /// The whole version part behind the operator
    pub(crate) version: Version,
}

#[cfg(feature = "pyo3")]
#[pymethods]
impl VersionSpecifier {
    // Since we don't bring FromStr to python
    /// Parse a PEP 440 version
    #[new]
    pub fn parse(version_specifier: String) -> PyResult {
        Self::from_str(&version_specifier).map_err(PyValueError::new_err)
    }

    /// See [VersionSpecifier::contains]
    #[pyo3(name = "contains")]
    pub fn py_contains(&self, version: &PyVersion) -> bool {
        self.contains(&version.0)
    }

    /// Whether the version fulfills the specifier
    pub fn __contains__(&self, version: &PyVersion) -> bool {
        self.contains(&version.0)
    }

    /// Returns the normalized representation
    pub fn __str__(&self) -> String {
        self.to_string()
    }

    /// Returns the normalized representation
    pub fn __repr__(&self) -> String {
        format!(r#""#, self)
    }

    fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult {
        if matches!(op, CompareOp::Eq) {
            Ok(self == other)
        } else {
            Err(PyNotImplementedError::new_err(
                "Can only compare VersionSpecifier by equality",
            ))
        }
    }

    /// Returns the normalized representation
    pub fn __hash__(&self) -> u64 {
        let mut hasher = DefaultHasher::new();
        self.hash(&mut hasher);
        hasher.finish()
    }
}

/// https://github.com/serde-rs/serde/issues/1316#issue-332908452
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for VersionSpecifier {
    fn deserialize(deserializer: D) -> Result
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        FromStr::from_str(&s).map_err(de::Error::custom)
    }
}

/// https://github.com/serde-rs/serde/issues/1316#issue-332908452
#[cfg(feature = "serde")]
impl Serialize for VersionSpecifier {
    fn serialize(&self, serializer: S) -> Result
    where
        S: Serializer,
    {
        serializer.collect_str(self)
    }
}

impl VersionSpecifier {
    /// Build from parts, validating that the operator is allowed with that version. The last
    /// parameter indicates a trailing `.*`, to differentiate between `1.1.*` and `1.1`
    pub fn new(operator: Operator, version: Version, star: bool) -> Result {
        // "Local version identifiers are NOT permitted in this version specifier."
        if let Some(local) = &version.local {
            if matches!(
                operator,
                Operator::GreaterThan
                    | Operator::GreaterThanEqual
                    | Operator::LessThan
                    | Operator::LessThanEqual
                    | Operator::TildeEqual
                    | Operator::EqualStar
                    | Operator::NotEqualStar
            ) {
                return Err(format!(
                    "You can't mix a {} operator with a local version (`+{}`)",
                    operator,
                    local
                        .iter()
                        .map(|x| x.to_string())
                        .collect::>()
                        .join(".")
                ));
            }
        }

        // Check if there are star versions and if so, switch operator to star version
        let operator = if star {
            match operator {
                Operator::Equal => Operator::EqualStar,
                Operator::NotEqual => Operator::NotEqualStar,
                other => {
                    return Err(format!(
                        "Operator {} must not be used in version ending with a star",
                        other
                    ))
                }
            }
        } else {
            operator
        };

        if operator == Operator::TildeEqual && version.release.len() < 2 {
            return Err(
                "The ~= operator requires at least two parts in the release version".to_string(),
            );
        }

        Ok(Self { operator, version })
    }

    /// Get the operator, e.g. `>=` in `>= 2.0.0`
    pub fn operator(&self) -> &Operator {
        &self.operator
    }

    /// Get the version, e.g. `<=` in `<= 2.0.0`
    pub fn version(&self) -> &Version {
        &self.version
    }
}

impl VersionSpecifier {
    /// Whether the given version satisfies the version range
    ///
    /// e.g. `>=1.19,<2.0` and `1.21` -> true
    /// 
    ///
    /// Unlike `pypa/packaging`, prereleases are included by default
    ///
    /// I'm utterly non-confident in the description in PEP 440 and apparently even pip got some
    /// of that wrong, e.g.  and
    /// , and also i'm not sure if it produces the correct
    /// behaviour from a user perspective
    ///
    /// This implementation is as close as possible to
    /// 
    pub fn contains(&self, version: &Version) -> bool {
        // "Except where specifically noted below, local version identifiers MUST NOT be permitted
        // in version specifiers, and local version labels MUST be ignored entirely when checking
        // if candidate versions match a given version specifier."
        let (this, other) = if self.version.local.is_some() {
            (self.version.clone(), version.clone())
        } else {
            // self is already without local
            (self.version.without_local(), version.without_local())
        };

        match self.operator {
            Operator::Equal => other == this,
            Operator::EqualStar => {
                this.epoch == other.epoch
                    && self
                        .version
                        .release
                        .iter()
                        .zip(&other.release)
                        .all(|(this, other)| this == other)
            }
            #[allow(deprecated)]
            Operator::ExactEqual => {
                #[cfg(feature = "tracing")]
                {
                    warn!("Using arbitrary equality (`===`) is discouraged");
                }
                self.version.to_string() == version.to_string()
            }
            Operator::NotEqual => other != this,
            Operator::NotEqualStar => {
                this.epoch != other.epoch
                    || !this
                        .release
                        .iter()
                        .zip(&version.release)
                        .all(|(this, other)| this == other)
            }
            Operator::TildeEqual => {
                // "For a given release identifier V.N, the compatible release clause is
                // approximately equivalent to the pair of comparison clauses: `>= V.N, == V.*`"
                // First, we test that every but the last digit matches.
                // We know that this must hold true since we checked it in the constructor
                assert!(this.release.len() > 1);
                if this.epoch != other.epoch {
                    return false;
                }

                if !this.release[..this.release.len() - 1]
                    .iter()
                    .zip(&other.release)
                    .all(|(this, other)| this == other)
                {
                    return false;
                }

                // According to PEP 440, this ignores the prerelease special rules
                // pypa/packaging disagrees: https://github.com/pypa/packaging/issues/617
                other >= this
            }
            Operator::GreaterThan => Self::greater_than(&this, &other),
            Operator::GreaterThanEqual => Self::greater_than(&this, &other) || other >= this,
            Operator::LessThan => {
                Self::less_than(&this, &other)
                    && !(version::compare_release(&this.release, &other.release) == Ordering::Equal
                        && other.any_prerelease())
            }
            Operator::LessThanEqual => Self::less_than(&this, &other) || other <= this,
        }
    }

    fn less_than(this: &Version, other: &Version) -> bool {
        if other.epoch < this.epoch {
            return true;
        }

        // This special case is here so that, unless the specifier itself
        // includes is a pre-release version, that we do not accept pre-release
        // versions for the version mentioned in the specifier (e.g. <3.1 should
        // not match 3.1.dev0, but should match 3.0.dev0).
        if !this.any_prerelease()
            && other.is_pre()
            && version::compare_release(&this.release, &other.release) == Ordering::Equal
        {
            return false;
        }

        other < this
    }

    fn greater_than(this: &Version, other: &Version) -> bool {
        if other.epoch > this.epoch {
            return true;
        }

        if version::compare_release(&this.release, &other.release) == Ordering::Equal {
            // This special case is here so that, unless the specifier itself
            // includes is a post-release version, that we do not accept
            // post-release versions for the version mentioned in the specifier
            // (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).
            if !this.is_post() && other.is_post() {
                return false;
            }

            // We already checked that self doesn't have a local version
            if other.is_local() {
                return false;
            }
        }

        other > this
    }
}

impl FromStr for VersionSpecifier {
    type Err = String;

    /// Parses a version such as `>= 1.19`, `== 1.1.*`,`~=1.0+abc.5` or `<=1!2012.2`
    fn from_str(spec: &str) -> Result {
        let captures = VERSION_SPECIFIER_RE
            .captures(spec)
            .ok_or_else(|| format!("Version specifier `{}` doesn't match PEP 440 rules", spec))?;
        let (version, star) = Version::parse_impl(&captures)?;
        // operator but we don't know yet if it has a star
        let operator = Operator::from_str(&captures["operator"])?;
        let version_specifier = VersionSpecifier::new(operator, version, star)?;
        Ok(version_specifier)
    }
}

impl Display for VersionSpecifier {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if self.operator == Operator::EqualStar || self.operator == Operator::NotEqualStar {
            return write!(f, "{}{}.*", self.operator, self.version);
        }
        write!(f, "{}{}", self.operator, self.version)
    }
}

/// Parses a list of specifiers such as `>= 1.0, != 1.3.*, < 2.0`.
///
/// I recommend using [VersionSpecifiers::from_str] instead.
///
/// ```rust
/// use std::str::FromStr;
/// use pep440_rs::{parse_version_specifiers, Version};
///
/// let version = Version::from_str("1.19").unwrap();
/// let version_specifiers = parse_version_specifiers(">=1.16, <2.0").unwrap();
/// assert!(version_specifiers.iter().all(|specifier| specifier.contains(&version)));
/// ```
pub fn parse_version_specifiers(spec: &str) -> Result, Pep440Error> {
    let mut version_ranges = Vec::new();
    let mut start: usize = 0;
    let separator = ",";
    for version_range_spec in spec.split(separator) {
        match VersionSpecifier::from_str(version_range_spec) {
            Err(err) => {
                return Err(Pep440Error {
                    message: err,
                    line: spec.to_string(),
                    start,
                    width: version_range_spec.width(),
                });
            }
            Ok(version_range) => {
                version_ranges.push(version_range);
            }
        }
        start += version_range_spec.width();
        start += separator.width();
    }
    Ok(version_ranges)
}

#[cfg(test)]
mod test {
    use crate::{Operator, Version, VersionSpecifier, VersionSpecifiers};
    use indoc::indoc;
    use std::cmp::Ordering;
    use std::str::FromStr;

    /// 
    #[test]
    fn test_equal() {
        let version = Version::from_str("1.1.post1").unwrap();

        assert!(!VersionSpecifier::from_str("== 1.1")
            .unwrap()
            .contains(&version));
        assert!(VersionSpecifier::from_str("== 1.1.post1")
            .unwrap()
            .contains(&version));
        assert!(VersionSpecifier::from_str("== 1.1.*")
            .unwrap()
            .contains(&version));
    }

    const VERSIONS_ALL: &[&str] = &[
        // Implicit epoch of 0
        "1.0.dev456",
        "1.0a1",
        "1.0a2.dev456",
        "1.0a12.dev456",
        "1.0a12",
        "1.0b1.dev456",
        "1.0b2",
        "1.0b2.post345.dev456",
        "1.0b2.post345",
        "1.0b2-346",
        "1.0c1.dev456",
        "1.0c1",
        "1.0rc2",
        "1.0c3",
        "1.0",
        "1.0.post456.dev34",
        "1.0.post456",
        "1.1.dev1",
        "1.2+123abc",
        "1.2+123abc456",
        "1.2+abc",
        "1.2+abc123",
        "1.2+abc123def",
        "1.2+1234.abc",
        "1.2+123456",
        "1.2.r32+123456",
        "1.2.rev33+123456",
        // Explicit epoch of 1
        "1!1.0.dev456",
        "1!1.0a1",
        "1!1.0a2.dev456",
        "1!1.0a12.dev456",
        "1!1.0a12",
        "1!1.0b1.dev456",
        "1!1.0b2",
        "1!1.0b2.post345.dev456",
        "1!1.0b2.post345",
        "1!1.0b2-346",
        "1!1.0c1.dev456",
        "1!1.0c1",
        "1!1.0rc2",
        "1!1.0c3",
        "1!1.0",
        "1!1.0.post456.dev34",
        "1!1.0.post456",
        "1!1.1.dev1",
        "1!1.2+123abc",
        "1!1.2+123abc456",
        "1!1.2+abc",
        "1!1.2+abc123",
        "1!1.2+abc123def",
        "1!1.2+1234.abc",
        "1!1.2+123456",
        "1!1.2.r32+123456",
        "1!1.2.rev33+123456",
    ];

    /// https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L666-L707
    /// https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L709-L750
    ///
    /// These tests are a lot shorter than the pypa/packaging version since we implement all
    /// comparisons through one method
    #[test]
    fn test_operators_true() {
        let versions: Vec = VERSIONS_ALL
            .iter()
            .map(|version| Version::from_str(version).unwrap())
            .collect();

        // Below we'll generate every possible combination of VERSIONS_ALL that
        // should be true for the given operator
        let operations: Vec<_> = [
            // Verify that the less than (<) operator works correctly
            versions
                .iter()
                .enumerate()
                .flat_map(|(i, x)| {
                    versions[i + 1..]
                        .iter()
                        .map(move |y| (x, y, Ordering::Less))
                })
                .collect::>(),
            // Verify that the equal (==) operator works correctly
            versions
                .iter()
                .map(move |x| (x, x, Ordering::Equal))
                .collect::>(),
            // Verify that the greater than (>) operator works correctly
            versions
                .iter()
                .enumerate()
                .flat_map(|(i, x)| versions[..i].iter().map(move |y| (x, y, Ordering::Greater)))
                .collect::>(),
        ]
        .into_iter()
        .flatten()
        .collect();

        for (a, b, ordering) in operations {
            assert_eq!(a.cmp(b), ordering, "{} {:?} {}", a, ordering, b);
        }
    }

    const VERSIONS_0: &[&str] = &[
        "1.0.dev456",
        "1.0a1",
        "1.0a2.dev456",
        "1.0a12.dev456",
        "1.0a12",
        "1.0b1.dev456",
        "1.0b2",
        "1.0b2.post345.dev456",
        "1.0b2.post345",
        "1.0b2-346",
        "1.0c1.dev456",
        "1.0c1",
        "1.0rc2",
        "1.0c3",
        "1.0",
        "1.0.post456.dev34",
        "1.0.post456",
        "1.1.dev1",
        "1.2+123abc",
        "1.2+123abc456",
        "1.2+abc",
        "1.2+abc123",
        "1.2+abc123def",
        "1.2+1234.abc",
        "1.2+123456",
        "1.2.r32+123456",
        "1.2.rev33+123456",
    ];

    const SPECIFIERS_OTHER: &[&str] = &[
        "== 1.*", "== 1.0.*", "== 1.1.*", "== 1.2.*", "== 2.*", "~= 1.0", "~= 1.0b1", "~= 1.1",
        "~= 1.2", "~= 2.0",
    ];

    const EXPECTED_OTHER: &[[bool; 10]] = &[
        [
            true, true, false, false, false, false, false, false, false, false,
        ],
        [
            true, true, false, false, false, false, false, false, false, false,
        ],
        [
            true, true, false, false, false, false, false, false, false, false,
        ],
        [
            true, true, false, false, false, false, false, false, false, false,
        ],
        [
            true, true, false, false, false, false, false, false, false, false,
        ],
        [
            true, true, false, false, false, false, false, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, false, true, false, false, false,
        ],
        [
            true, true, false, false, false, true, true, false, false, false,
        ],
        [
            true, true, false, false, false, true, true, false, false, false,
        ],
        [
            true, true, false, false, false, true, true, false, false, false,
        ],
        [
            true, false, true, false, false, true, true, false, false, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
        [
            true, false, false, true, false, true, true, true, true, false,
        ],
    ];

    /// Test for tilde equal (~=) and star equal (== x.y.*) recorded from pypa/packaging
    ///
    /// Well, except for https://github.com/pypa/packaging/issues/617
    #[test]
    fn test_operators_other() {
        let versions: Vec = VERSIONS_0
            .iter()
            .map(|version| Version::from_str(version).unwrap())
            .collect();
        let specifiers: Vec<_> = SPECIFIERS_OTHER
            .iter()
            .map(|specifier| VersionSpecifier::from_str(specifier).unwrap())
            .collect();

        for (version, expected) in versions.iter().zip(EXPECTED_OTHER) {
            let actual = specifiers
                .iter()
                .map(|specifier| specifier.contains(version))
                .collect::>();
            for ((actual, expected), specifier) in actual.iter().zip(expected).zip(SPECIFIERS_OTHER)
            {
                assert_eq!(actual, expected, "{} {}", version, specifier);
            }
        }
    }

    #[test]
    fn test_arbitrary_equality() {
        assert!(VersionSpecifier::from_str("=== 1.2a1")
            .unwrap()
            .contains(&Version::from_str("1.2a1").unwrap()));
        assert!(!VersionSpecifier::from_str("=== 1.2a1")
            .unwrap()
            .contains(&Version::from_str("1.2a1+local").unwrap()));
    }

    #[test]
    fn test_specifiers_true() {
        let pairs = [
            // Test the equality operation
            ("2.0", "==2"),
            ("2.0", "==2.0"),
            ("2.0", "==2.0.0"),
            ("2.0+deadbeef", "==2"),
            ("2.0+deadbeef", "==2.0"),
            ("2.0+deadbeef", "==2.0.0"),
            ("2.0+deadbeef", "==2+deadbeef"),
            ("2.0+deadbeef", "==2.0+deadbeef"),
            ("2.0+deadbeef", "==2.0.0+deadbeef"),
            ("2.0+deadbeef.0", "==2.0.0+deadbeef.00"),
            // Test the equality operation with a prefix
            ("2.dev1", "==2.*"),
            ("2a1", "==2.*"),
            ("2a1.post1", "==2.*"),
            ("2b1", "==2.*"),
            ("2b1.dev1", "==2.*"),
            ("2c1", "==2.*"),
            ("2c1.post1.dev1", "==2.*"),
            ("2c1.post1.dev1", "==2.0.*"),
            ("2rc1", "==2.*"),
            ("2rc1", "==2.0.*"),
            ("2", "==2.*"),
            ("2", "==2.0.*"),
            ("2", "==0!2.*"),
            ("0!2", "==2.*"),
            ("2.0", "==2.*"),
            ("2.0.0", "==2.*"),
            ("2.1+local.version", "==2.1.*"),
            // Test the in-equality operation
            ("2.1", "!=2"),
            ("2.1", "!=2.0"),
            ("2.0.1", "!=2"),
            ("2.0.1", "!=2.0"),
            ("2.0.1", "!=2.0.0"),
            ("2.0", "!=2.0+deadbeef"),
            // Test the in-equality operation with a prefix
            ("2.0", "!=3.*"),
            ("2.1", "!=2.0.*"),
            // Test the greater than equal operation
            ("2.0", ">=2"),
            ("2.0", ">=2.0"),
            ("2.0", ">=2.0.0"),
            ("2.0.post1", ">=2"),
            ("2.0.post1.dev1", ">=2"),
            ("3", ">=2"),
            // Test the less than equal operation
            ("2.0", "<=2"),
            ("2.0", "<=2.0"),
            ("2.0", "<=2.0.0"),
            ("2.0.dev1", "<=2"),
            ("2.0a1", "<=2"),
            ("2.0a1.dev1", "<=2"),
            ("2.0b1", "<=2"),
            ("2.0b1.post1", "<=2"),
            ("2.0c1", "<=2"),
            ("2.0c1.post1.dev1", "<=2"),
            ("2.0rc1", "<=2"),
            ("1", "<=2"),
            // Test the greater than operation
            ("3", ">2"),
            ("2.1", ">2.0"),
            ("2.0.1", ">2"),
            ("2.1.post1", ">2"),
            ("2.1+local.version", ">2"),
            // Test the less than operation
            ("1", "<2"),
            ("2.0", "<2.1"),
            ("2.0.dev0", "<2.1"),
            // Test the compatibility operation
            ("1", "~=1.0"),
            ("1.0.1", "~=1.0"),
            ("1.1", "~=1.0"),
            ("1.9999999", "~=1.0"),
            ("1.1", "~=1.0a1"),
            ("2022.01.01", "~=2022.01.01"),
            // Test that epochs are handled sanely
            ("2!1.0", "~=2!1.0"),
            ("2!1.0", "==2!1.*"),
            ("2!1.0", "==2!1.0"),
            ("2!1.0", "!=1.0"),
            ("1.0", "!=2!1.0"),
            ("1.0", "<=2!0.1"),
            ("2!1.0", ">=2.0"),
            ("1.0", "<2!0.1"),
            ("2!1.0", ">2.0"),
            // Test some normalization rules
            ("2.0.5", ">2.0dev"),
        ];

        for (version, specifier) in pairs {
            assert!(
                VersionSpecifier::from_str(specifier)
                    .unwrap()
                    .contains(&Version::from_str(version).unwrap()),
                "{} {}",
                version,
                specifier
            );
        }
    }

    #[test]
    fn test_specifier_false() {
        let pairs = [
            // Test the equality operation
            ("2.1", "==2"),
            ("2.1", "==2.0"),
            ("2.1", "==2.0.0"),
            ("2.0", "==2.0+deadbeef"),
            // Test the equality operation with a prefix
            ("2.0", "==3.*"),
            ("2.1", "==2.0.*"),
            // Test the in-equality operation
            ("2.0", "!=2"),
            ("2.0", "!=2.0"),
            ("2.0", "!=2.0.0"),
            ("2.0+deadbeef", "!=2"),
            ("2.0+deadbeef", "!=2.0"),
            ("2.0+deadbeef", "!=2.0.0"),
            ("2.0+deadbeef", "!=2+deadbeef"),
            ("2.0+deadbeef", "!=2.0+deadbeef"),
            ("2.0+deadbeef", "!=2.0.0+deadbeef"),
            ("2.0+deadbeef.0", "!=2.0.0+deadbeef.00"),
            // Test the in-equality operation with a prefix
            ("2.dev1", "!=2.*"),
            ("2a1", "!=2.*"),
            ("2a1.post1", "!=2.*"),
            ("2b1", "!=2.*"),
            ("2b1.dev1", "!=2.*"),
            ("2c1", "!=2.*"),
            ("2c1.post1.dev1", "!=2.*"),
            ("2c1.post1.dev1", "!=2.0.*"),
            ("2rc1", "!=2.*"),
            ("2rc1", "!=2.0.*"),
            ("2", "!=2.*"),
            ("2", "!=2.0.*"),
            ("2.0", "!=2.*"),
            ("2.0.0", "!=2.*"),
            // Test the greater than equal operation
            ("2.0.dev1", ">=2"),
            ("2.0a1", ">=2"),
            ("2.0a1.dev1", ">=2"),
            ("2.0b1", ">=2"),
            ("2.0b1.post1", ">=2"),
            ("2.0c1", ">=2"),
            ("2.0c1.post1.dev1", ">=2"),
            ("2.0rc1", ">=2"),
            ("1", ">=2"),
            // Test the less than equal operation
            ("2.0.post1", "<=2"),
            ("2.0.post1.dev1", "<=2"),
            ("3", "<=2"),
            // Test the greater than operation
            ("1", ">2"),
            ("2.0.dev1", ">2"),
            ("2.0a1", ">2"),
            ("2.0a1.post1", ">2"),
            ("2.0b1", ">2"),
            ("2.0b1.dev1", ">2"),
            ("2.0c1", ">2"),
            ("2.0c1.post1.dev1", ">2"),
            ("2.0rc1", ">2"),
            ("2.0", ">2"),
            ("2.0.post1", ">2"),
            ("2.0.post1.dev1", ">2"),
            ("2.0+local.version", ">2"),
            // Test the less than operation
            ("2.0.dev1", "<2"),
            ("2.0a1", "<2"),
            ("2.0a1.post1", "<2"),
            ("2.0b1", "<2"),
            ("2.0b2.dev1", "<2"),
            ("2.0c1", "<2"),
            ("2.0c1.post1.dev1", "<2"),
            ("2.0rc1", "<2"),
            ("2.0", "<2"),
            ("2.post1", "<2"),
            ("2.post1.dev1", "<2"),
            ("3", "<2"),
            // Test the compatibility operation
            ("2.0", "~=1.0"),
            ("1.1.0", "~=1.0.0"),
            ("1.1.post1", "~=1.0.0"),
            // Test that epochs are handled sanely
            ("1.0", "~=2!1.0"),
            ("2!1.0", "~=1.0"),
            ("2!1.0", "==1.0"),
            ("1.0", "==2!1.0"),
            ("2!1.0", "==1.*"),
            ("1.0", "==2!1.*"),
            ("2!1.0", "!=2!1.0"),
        ];
        for (version, specifier) in pairs {
            assert!(
                !VersionSpecifier::from_str(specifier)
                    .unwrap()
                    .contains(&Version::from_str(version).unwrap()),
                "{} {}",
                version,
                specifier
            );
        }
    }

    #[test]
    fn test_parse_version_specifiers() {
        let result = VersionSpecifiers::from_str("~= 0.9, >= 1.0, != 1.3.4.*, < 2.0").unwrap();
        assert_eq!(
            result.0,
            [
                VersionSpecifier {
                    operator: Operator::TildeEqual,
                    version: Version {
                        epoch: 0,
                        release: vec![0, 9],
                        pre: None,
                        post: None,
                        dev: None,
                        local: None
                    }
                },
                VersionSpecifier {
                    operator: Operator::GreaterThanEqual,
                    version: Version {
                        epoch: 0,
                        release: vec![1, 0],
                        pre: None,
                        post: None,
                        dev: None,
                        local: None
                    }
                },
                VersionSpecifier {
                    operator: Operator::NotEqualStar,
                    version: Version {
                        epoch: 0,
                        release: vec![1, 3, 4],
                        pre: None,
                        post: None,
                        dev: None,
                        local: None
                    }
                },
                VersionSpecifier {
                    operator: Operator::LessThan,
                    version: Version {
                        epoch: 0,
                        release: vec![2, 0],
                        pre: None,
                        post: None,
                        dev: None,
                        local: None
                    }
                }
            ]
        );
    }

    #[test]
    fn test_parse_error() {
        let result = VersionSpecifiers::from_str("~= 0.9, %‍= 1.0, != 1.3.4.*");
        assert_eq!(
            result.unwrap_err().to_string(),
            indoc! {r#"
                Failed to parse version:
                ~= 0.9, %‍= 1.0, != 1.3.4.*
                       ^^^^^^^
            "#}
        );
    }

    #[test]
    fn test_non_star_after_star() {
        let result = VersionSpecifiers::from_str("== 0.9.*.1");
        assert_eq!(
            result.unwrap_err().message,
            "Version specifier `== 0.9.*.1` doesn't match PEP 440 rules"
        );
    }

    #[test]
    fn test_star_wrong_operator() {
        let result = VersionSpecifiers::from_str(">= 0.9.1.*");
        assert_eq!(
            result.unwrap_err().message,
            "Operator >= must not be used in version ending with a star"
        );
    }

    #[test]
    fn test_regex_mismatch() {
        let result = VersionSpecifiers::from_str("blergh");
        assert_eq!(
            result.unwrap_err().message,
            "Version specifier `blergh` doesn't match PEP 440 rules"
        );
    }

    /// 
    #[test]
    fn test_invalid_specifier() {
        let specifiers = [
            // Operator-less specifier
            ("2.0", None),
            // Invalid operator
            ("=>2.0", None),
            // Version-less specifier
            ("==", None),
            // Local segment on operators which don't support them
            (
                "~=1.0+5",
                Some("You can't mix a ~= operator with a local version (`+5`)"),
            ),
            (
                ">=1.0+deadbeef",
                Some("You can't mix a >= operator with a local version (`+deadbeef`)"),
            ),
            (
                "<=1.0+abc123",
                Some("You can't mix a <= operator with a local version (`+abc123`)"),
            ),
            (
                ">1.0+watwat",
                Some("You can't mix a > operator with a local version (`+watwat`)"),
            ),
            (
                "<1.0+1.0",
                Some("You can't mix a < operator with a local version (`+1.0`)"),
            ),
            // Prefix matching on operators which don't support them
            (
                "~=1.0.*",
                Some("Operator ~= must not be used in version ending with a star"),
            ),
            (
                ">=1.0.*",
                Some("Operator >= must not be used in version ending with a star"),
            ),
            (
                "<=1.0.*",
                Some("Operator <= must not be used in version ending with a star"),
            ),
            (
                ">1.0.*",
                Some("Operator > must not be used in version ending with a star"),
            ),
            (
                "<1.0.*",
                Some("Operator < must not be used in version ending with a star"),
            ),
            // Combination of local and prefix matching on operators which do
            // support one or the other
            (
                "==1.0.*+5",
                Some("Version specifier `==1.0.*+5` doesn't match PEP 440 rules"),
            ),
            (
                "!=1.0.*+deadbeef",
                Some("Version specifier `!=1.0.*+deadbeef` doesn't match PEP 440 rules"),
            ),
            // Prefix matching cannot be used with a pre-release, post-release,
            // dev or local version
            (
                "==2.0a1.*",
                Some("You can't have both a trailing `.*` and a prerelease version"),
            ),
            (
                "!=2.0a1.*",
                Some("You can't have both a trailing `.*` and a prerelease version"),
            ),
            (
                "==2.0.post1.*",
                Some("You can't have both a trailing `.*` and a post version"),
            ),
            (
                "!=2.0.post1.*",
                Some("You can't have both a trailing `.*` and a post version"),
            ),
            (
                "==2.0.dev1.*",
                Some("You can't have both a trailing `.*` and a dev version"),
            ),
            (
                "!=2.0.dev1.*",
                Some("You can't have both a trailing `.*` and a dev version"),
            ),
            (
                "==1.0+5.*",
                Some("You can't have both a trailing `.*` and a local version"),
            ),
            (
                "!=1.0+deadbeef.*",
                Some("You can't have both a trailing `.*` and a local version"),
            ),
            // Prefix matching must appear at the end
            (
                "==1.0.*.5",
                Some("Version specifier `==1.0.*.5` doesn't match PEP 440 rules"),
            ),
            // Compatible operator requires 2 digits in the release operator
            (
                "~=1",
                Some("The ~= operator requires at least two parts in the release version"),
            ),
            // Cannot use a prefix matching after a .devN version
            (
                "==1.0.dev1.*",
                Some("You can't have both a trailing `.*` and a dev version"),
            ),
            (
                "!=1.0.dev1.*",
                Some("You can't have both a trailing `.*` and a dev version"),
            ),
        ];
        for (specifier, error) in specifiers {
            if let Some(error) = error {
                assert_eq!(VersionSpecifier::from_str(specifier).unwrap_err(), error)
            } else {
                assert_eq!(
                    VersionSpecifier::from_str(specifier).unwrap_err(),
                    format!(
                        "Version specifier `{}` doesn't match PEP 440 rules",
                        specifier
                    )
                )
            }
        }
    }

    #[test]
    fn test_display_start() {
        assert_eq!(
            VersionSpecifier::from_str("==     1.1.*")
                .unwrap()
                .to_string(),
            "==1.1.*"
        );
        assert_eq!(
            VersionSpecifier::from_str("!=     1.1.*")
                .unwrap()
                .to_string(),
            "!=1.1.*"
        );
    }

    #[test]
    fn test_version_specifiers_str() {
        assert_eq!(
            VersionSpecifiers::from_str(">= 3.7").unwrap().to_string(),
            ">=3.7"
        );
        assert_eq!(
            VersionSpecifiers::from_str(">=3.7, <      4.0, != 3.9.0")
                .unwrap()
                .to_string(),
            ">=3.7, <4.0, !=3.9.0"
        );
    }
}