From f03398bee3c03fd3351855af73d53f2a149d8d02 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 6 Oct 2023 20:11:52 -0400 Subject: [PATCH] Copy over `pep440-rs` crate (#30) This PR copies over the `pep440-rs` crate at commit `a8303b01ffef6fccfdce562a887f6b110d482ef3` with no modifications. It won't pass CI, but modifications will intentionally be confined to later PRs. --- crates/README.md | 4 + crates/pep440-rs/Cargo.lock | 410 ++++++ crates/pep440-rs/Cargo.toml | 25 + crates/pep440-rs/Changelog.md | 51 + crates/pep440-rs/License-Apache | 176 +++ crates/pep440-rs/License-BSD | 22 + crates/pep440-rs/Readme.md | 56 + crates/pep440-rs/pyproject.toml | 15 + crates/pep440-rs/python/Readme.md | 42 + crates/pep440-rs/python/pep440_rs/__init__.py | 22 + .../pep440-rs/python/pep440_rs/__init__.pyi | 40 + crates/pep440-rs/src/lib.rs | 91 ++ crates/pep440-rs/src/version.rs | 1187 +++++++++++++++ crates/pep440-rs/src/version_specifier.rs | 1291 +++++++++++++++++ crates/pep440-rs/test/test_python.py | 48 + 15 files changed, 3480 insertions(+) create mode 100644 crates/pep440-rs/Cargo.lock create mode 100644 crates/pep440-rs/Cargo.toml create mode 100644 crates/pep440-rs/Changelog.md create mode 100644 crates/pep440-rs/License-Apache create mode 100644 crates/pep440-rs/License-BSD create mode 100644 crates/pep440-rs/Readme.md create mode 100644 crates/pep440-rs/pyproject.toml create mode 100644 crates/pep440-rs/python/Readme.md create mode 100644 crates/pep440-rs/python/pep440_rs/__init__.py create mode 100644 crates/pep440-rs/python/pep440_rs/__init__.pyi create mode 100644 crates/pep440-rs/src/lib.rs create mode 100644 crates/pep440-rs/src/version.rs create mode 100644 crates/pep440-rs/src/version_specifier.rs create mode 100644 crates/pep440-rs/test/test_python.py diff --git a/crates/README.md b/crates/README.md index 418de43d8..23e8f3cd2 100644 --- a/crates/README.md +++ b/crates/README.md @@ -1,5 +1,9 @@ # Crates +## [pep440-rs](./pep440-rs) + +Utilities for interacting with Python version numbers and specifiers. + ## [puffin-cli](./puffin-cli) Command-line interface for the Puffin package manager. diff --git a/crates/pep440-rs/Cargo.lock b/crates/pep440-rs/Cargo.lock new file mode 100644 index 000000000..7427c776d --- /dev/null +++ b/crates/pep440-rs/Cargo.lock @@ -0,0 +1,410 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pep440_rs" +version = "0.3.12" +dependencies = [ + "indoc 2.0.4", + "lazy_static", + "pyo3", + "regex", + "serde", + "tracing", + "unicode-width", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" +dependencies = [ + "cfg-if", + "indoc 1.0.9", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unindent" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/crates/pep440-rs/Cargo.toml b/crates/pep440-rs/Cargo.toml new file mode 100644 index 000000000..bf6297ab8 --- /dev/null +++ b/crates/pep440-rs/Cargo.toml @@ -0,0 +1,25 @@ +[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" diff --git a/crates/pep440-rs/Changelog.md b/crates/pep440-rs/Changelog.md new file mode 100644 index 000000000..f45f3f2c9 --- /dev/null +++ b/crates/pep440-rs/Changelog.md @@ -0,0 +1,51 @@ +## 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 module \ No newline at end of file diff --git a/crates/pep440-rs/License-Apache b/crates/pep440-rs/License-Apache new file mode 100644 index 000000000..d9a10c0d8 --- /dev/null +++ b/crates/pep440-rs/License-Apache @@ -0,0 +1,176 @@ + 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 diff --git a/crates/pep440-rs/License-BSD b/crates/pep440-rs/License-BSD new file mode 100644 index 000000000..76f5704dd --- /dev/null +++ b/crates/pep440-rs/License-BSD @@ -0,0 +1,22 @@ +Copyright (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. diff --git a/crates/pep440-rs/Readme.md b/crates/pep440-rs/Readme.md new file mode 100644 index 000000000..295f0b4c5 --- /dev/null +++ b/crates/pep440-rs/Readme.md @@ -0,0 +1,56 @@ +# 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 diff --git a/crates/pep440-rs/pyproject.toml b/crates/pep440-rs/pyproject.toml new file mode 100644 index 000000000..681bccef1 --- /dev/null +++ b/crates/pep440-rs/pyproject.toml @@ -0,0 +1,15 @@ +[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"] diff --git a/crates/pep440-rs/python/Readme.md b/crates/pep440-rs/python/Readme.md new file mode 100644 index 000000000..01a88d249 --- /dev/null +++ b/crates/pep440-rs/python/Readme.md @@ -0,0 +1,42 @@ +# 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 diff --git a/crates/pep440-rs/python/pep440_rs/__init__.py b/crates/pep440-rs/python/pep440_rs/__init__.py new file mode 100644 index 000000000..da3c87604 --- /dev/null +++ b/crates/pep440-rs/python/pep440_rs/__init__.py @@ -0,0 +1,22 @@ +""" +A PEP440 reimplementation in rust + +```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") +``` + +""" + +from ._pep440_rs import * + +__doc__ = _pep440_rs.__doc__ +if hasattr(_pep440_rs, "__all__"): + __all__ = _pep440_rs.__all__ diff --git a/crates/pep440-rs/python/pep440_rs/__init__.pyi b/crates/pep440-rs/python/pep440_rs/__init__.pyi new file mode 100644 index 000000000..c4796455c --- /dev/null +++ b/crates/pep440-rs/python/pep440_rs/__init__.pyi @@ -0,0 +1,40 @@ +# Generated by `stubgen -p pep440_rs` +from typing import Any, ClassVar + + +class Version: + dev: Any + epoch: Any + post: Any + pre: Any + release: Any + major: Any + minor: Any + micro: Any + + @classmethod + def __init__(cls, *args, **kwargs) -> None: ... + def any_prerelease(self, *args, **kwargs) -> Any: ... + def parse_star(self, *args, **kwargs) -> Any: ... + def __eq__(self, other) -> Any: ... + def __ge__(self, other) -> Any: ... + def __gt__(self, other) -> Any: ... + def __hash__(self) -> Any: ... + def __le__(self, other) -> Any: ... + def __lt__(self, other) -> Any: ... + def __ne__(self, other) -> Any: ... + + +class VersionSpecifier: + __hash__: ClassVar[None] = ... + + @classmethod + def __init__(cls, *args, **kwargs) -> None: ... + def contains(self, *args, **kwargs) -> Any: ... + def __contains__(self, other) -> Any: ... + def __eq__(self, other) -> Any: ... + def __ge__(self, other) -> Any: ... + def __gt__(self, other) -> Any: ... + def __le__(self, other) -> Any: ... + def __lt__(self, other) -> Any: ... + def __ne__(self, other) -> Any: ... diff --git a/crates/pep440-rs/src/lib.rs b/crates/pep440-rs/src/lib.rs new file mode 100644 index 000000000..5fcfe90af --- /dev/null +++ b/crates/pep440-rs/src/lib.rs @@ -0,0 +1,91 @@ +//! 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(()) +} diff --git a/crates/pep440-rs/src/version.rs b/crates/pep440-rs/src/version.rs new file mode 100644 index 000000000..93f4b569a --- /dev/null +++ b/crates/pep440-rs/src/version.rs @@ -0,0 +1,1187 @@ +use 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
+    }
+}
diff --git a/crates/pep440-rs/src/version_specifier.rs b/crates/pep440-rs/src/version_specifier.rs
new file mode 100644
index 000000000..68eb72115
--- /dev/null
+++ b/crates/pep440-rs/src/version_specifier.rs
@@ -0,0 +1,1291 @@
+#[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()
+    }
+
+    /// Whether the version matches all the specifiers
+    pub fn __contains__(&self, version: &PyVersion) -> bool {
+        self.contains(&version.0)
+    }
+}
+
+#[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"
+        );
+    }
+}
diff --git a/crates/pep440-rs/test/test_python.py b/crates/pep440-rs/test/test_python.py
new file mode 100644
index 000000000..0b031cdb5
--- /dev/null
+++ b/crates/pep440-rs/test/test_python.py
@@ -0,0 +1,48 @@
+"""
+This is implementation has some very rudimentary python bindings
+"""
+from pep440_rs import Version, VersionSpecifier, Operator, VersionSpecifiers
+
+
+def test_pep440():
+    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("1.1") >= Version("1.1a1")
+    assert Version("2.0") in VersionSpecifier("==2")
+    assert Version("2.1").major == 2
+    assert Version("2.1").minor == 1
+    assert Version("2.1").micro == 0
+
+
+def test_version_specifier():
+    assert VersionSpecifier(">=1.1").version == Version("1.1")
+    assert VersionSpecifier(">=1.1").operator == Operator.GreaterThanEqual
+    assert str(VersionSpecifier(">=1.1").operator) == ">="
+    # Note: This removes the star
+    assert VersionSpecifier("==1.1.*").version == Version("1.1")
+    assert str(VersionSpecifier("==1.1.*").operator) == "=="
+    assert {
+        VersionSpecifier("==1.1.*"),
+        VersionSpecifier("==1.1"),
+        VersionSpecifier("==1.1"),
+    } == {VersionSpecifier("==1.1.*"), VersionSpecifier("==1.1")}
+
+
+def test_version_specifiers():
+    assert str(VersionSpecifiers(">=1.1, <2.0")) == ">=1.1, <2.0"
+    assert list(VersionSpecifiers(">=1.1, <2.0")) == [
+        VersionSpecifier(">=1.1"),
+        VersionSpecifier("<2.0"),
+    ]
+
+
+def test_normalization():
+    assert str(Version("1.19-alpha.1")) == "1.19a1"
+    assert str(VersionSpecifier(" >=1.19-alpha.1 ")) == ">=1.19a1"
+    assert repr(Version("1.19-alpha.1")) == ''
+    assert (
+        repr(VersionSpecifier(" >=1.19-alpha.1 ")) == '=1.19a1")>'
+    )