summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Klose <doko@debian.org>2021-11-27 09:14:41 +0100
committerMatthias Klose <doko@debian.org>2021-11-27 09:14:41 +0100
commit3c577b5ace7676dc680c162a39de78d5bb6e9b5e (patch)
tree2f66ae2b61daa6fc4be752f3f295ea4e93221c78
Import python-packaging_21.3.orig.tar.gz
[dgit import orig python-packaging_21.3.orig.tar.gz]
-rw-r--r--.coveragerc9
-rw-r--r--.flake83
-rw-r--r--.pre-commit-config.yaml39
-rw-r--r--CHANGELOG.rst347
-rw-r--r--CONTRIBUTING.rst23
-rw-r--r--LICENSE3
-rw-r--r--LICENSE.APACHE177
-rw-r--r--LICENSE.BSD23
-rw-r--r--MANIFEST.in24
-rw-r--r--PKG-INFO452
-rw-r--r--README.rst73
-rw-r--r--docs/Makefile153
-rw-r--r--docs/_static/.empty0
-rw-r--r--docs/changelog.rst1
-rw-r--r--docs/conf.py111
-rw-r--r--docs/development/getting-started.rst77
-rw-r--r--docs/development/index.rst19
-rw-r--r--docs/development/release-process.rst25
-rw-r--r--docs/development/reviewing-patches.rst37
-rw-r--r--docs/development/submitting-patches.rst74
-rw-r--r--docs/index.rst38
-rw-r--r--docs/markers.rst93
-rw-r--r--docs/requirements.rst89
-rw-r--r--docs/requirements.txt1
-rw-r--r--docs/security.rst18
-rw-r--r--docs/specifiers.rst222
-rw-r--r--docs/tags.rst225
-rw-r--r--docs/utils.rst92
-rw-r--r--docs/version.rst292
-rw-r--r--mypy.ini17
-rw-r--r--packaging.egg-info/PKG-INFO452
-rw-r--r--packaging.egg-info/SOURCES.txt73
-rw-r--r--packaging.egg-info/dependency_links.txt1
-rw-r--r--packaging.egg-info/requires.txt1
-rw-r--r--packaging.egg-info/top_level.txt1
-rw-r--r--packaging/__about__.py26
-rw-r--r--packaging/__init__.py25
-rw-r--r--packaging/_manylinux.py301
-rw-r--r--packaging/_musllinux.py136
-rw-r--r--packaging/_structures.py61
-rw-r--r--packaging/markers.py304
-rw-r--r--packaging/py.typed0
-rw-r--r--packaging/requirements.py146
-rw-r--r--packaging/specifiers.py802
-rw-r--r--packaging/tags.py487
-rw-r--r--packaging/utils.py136
-rw-r--r--packaging/version.py504
-rw-r--r--pyproject.toml3
-rw-r--r--setup.cfg8
-rw-r--r--setup.py70
-rw-r--r--tests/__init__.py3
-rwxr-xr-xtests/manylinux/hello-world-armv7l-armelbin0 -> 52 bytes
-rwxr-xr-xtests/manylinux/hello-world-armv7l-armhfbin0 -> 52 bytes
-rwxr-xr-xtests/manylinux/hello-world-invalid-classbin0 -> 52 bytes
-rwxr-xr-xtests/manylinux/hello-world-invalid-databin0 -> 52 bytes
-rwxr-xr-xtests/manylinux/hello-world-invalid-magicbin0 -> 52 bytes
-rw-r--r--tests/manylinux/hello-world-s390x-s390xbin0 -> 64 bytes
-rw-r--r--tests/manylinux/hello-world-too-shortbin0 -> 40 bytes
-rw-r--r--tests/manylinux/hello-world-x86_64-amd64bin0 -> 64 bytes
-rwxr-xr-xtests/manylinux/hello-world-x86_64-i386bin0 -> 52 bytes
-rwxr-xr-xtests/manylinux/hello-world-x86_64-x32bin0 -> 52 bytes
-rwxr-xr-xtests/musllinux/glibc-x86_64bin0 -> 1024 bytes
-rwxr-xr-xtests/musllinux/musl-aarch64bin0 -> 1024 bytes
-rwxr-xr-xtests/musllinux/musl-i386bin0 -> 1024 bytes
-rwxr-xr-xtests/musllinux/musl-x86_64bin0 -> 1024 bytes
-rw-r--r--tests/test_manylinux.py253
-rw-r--r--tests/test_markers.py310
-rw-r--r--tests/test_musllinux.py146
-rw-r--r--tests/test_requirements.py197
-rw-r--r--tests/test_specifiers.py998
-rw-r--r--tests/test_structures.py59
-rw-r--r--tests/test_tags.py1191
-rw-r--r--tests/test_utils.py124
-rw-r--r--tests/test_version.py904
74 files changed, 10479 insertions, 0 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..da205e5
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,9 @@
+[run]
+branch = True
+omit = packaging/_compat.py
+
+[report]
+exclude_lines =
+ pragma: no cover
+ @abc.abstractmethod
+ @abc.abstractproperty
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..b5a35be
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 88
+ignore = E203,W503,W504
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..49ae0d4
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,39 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v3.4.0
+ hooks:
+ - id: check-toml
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v0.812
+ hooks:
+ - id: mypy
+ exclude: '^(docs|tasks|tests)|setup\.py'
+ args: []
+
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.29.0
+ hooks:
+ - id: pyupgrade
+ args: [--py36-plus]
+
+ - repo: https://github.com/psf/black
+ rev: 20.8b1
+ hooks:
+ - id: black
+
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.8.0
+ hooks:
+ - id: isort
+
+ - repo: https://gitlab.com/PyCQA/flake8
+ rev: "3.9.0"
+ hooks:
+ - id: flake8
+ additional_dependencies: ["pep8-naming"]
+ # Ignore all format-related checks as Black takes care of those.
+ args: ["--ignore", "E2,W5", "--select", "E,W,F,N"]
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..f23c303
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,347 @@
+Changelog
+---------
+
+21.3 - 2021-11-17
+~~~~~~~~~~~~~~~~~
+
+* Add a ``pp3-none-any`` tag (:issue:`311`)
+* Replace the blank pyparsing 3 exclusion with a 3.0.5 exclusion (:issue:`481`, :issue:`486`)
+* Fix a spelling mistake (:issue:`479`)
+
+21.2 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update documentation entry for 21.1.
+
+21.1 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update pin to pyparsing to exclude 3.0.0.
+
+21.0 - 2021-07-03
+~~~~~~~~~~~~~~~~~
+
+* PEP 656: musllinux support (:issue:`411`)
+* Drop support for Python 2.7, Python 3.4 and Python 3.5.
+* Replace distutils usage with sysconfig (:issue:`396`)
+* Add support for zip files in ``parse_sdist_filename`` (:issue:`429`)
+* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (:issue:`417`)
+* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (:issue:`437`)
+* Proper keyword-only "warn" argument in packaging.tags (:issue:`403`)
+* Correctly remove prerelease suffixes from ~= check (:issue:`366`)
+* Fix type hints for ``Version.post`` and ``Version.dev`` (:issue:`393`)
+* Use typing alias ``UnparsedVersion`` (:issue:`398`)
+* Improve type inference for ``packaging.specifiers.filter()`` (:issue:`430`)
+* Tighten the return type of ``canonicalize_version()`` (:issue:`402`)
+
+20.9 - 2021-01-29
+~~~~~~~~~~~~~~~~~
+
+* Run `isort <https://pypi.org/project/isort/>`_ over the code base (:issue:`377`)
+* Add support for the ``macosx_10_*_universal2`` platform tags (:issue:`379`)
+* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()``
+ (:issue:`387` and :issue:`389`)
+
+20.8 - 2020-12-11
+~~~~~~~~~~~~~~~~~
+
+* Revert back to setuptools for compatibility purposes for some Linux distros (:issue:`363`)
+* Do not insert an underscore in wheel tags when the interpreter version number
+ is more than 2 digits (:issue:`372`)
+
+20.7 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+No unreleased changes.
+
+20.6 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+.. note:: This release was subsequently yanked, and these changes were included in 20.7.
+
+* Fix flit configuration, to include LICENSE files (:issue:`357`)
+* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (:issue:`361`)
+* Add some missing type hints to `packaging.requirements` (issue:`350`)
+
+20.5 - 2020-11-27
+~~~~~~~~~~~~~~~~~
+
+* Officially support Python 3.9 (:issue:`343`)
+* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (:issue:`321`)
+* Handle ``OSError`` on non-dynamic executables when attempting to resolve
+ the glibc version string.
+
+20.4 - 2020-05-19
+~~~~~~~~~~~~~~~~~
+
+* Canonicalize version before comparing specifiers. (:issue:`282`)
+* Change type hint for ``canonicalize_name`` to return
+ ``packaging.utils.NormalizedName``.
+ This enables the use of static typing tools (like mypy) to detect mixing of
+ normalized and un-normalized names.
+
+20.3 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix changelog for 20.2.
+
+20.2 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8,
+ aarch64), to report the wrong bitness.
+
+20.1 - 2020-01-24
+~~~~~~~~~~~~~~~~~~~
+
+* Fix a bug caused by reuse of an exhausted iterator. (:issue:`257`)
+
+20.0 - 2020-01-06
+~~~~~~~~~~~~~~~~~
+
+* Add type hints (:issue:`191`)
+
+* Add proper trove classifiers for PyPy support (:issue:`198`)
+
+* Scale back depending on ``ctypes`` for manylinux support detection (:issue:`171`)
+
+* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (:issue:`193`)
+
+* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (:issue:`187`)
+
+* Officially support Python 3.8 (:issue:`232`)
+
+* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (:issue:`226`)
+
+* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (:issue:`226`)
+
+19.2 - 2019-09-18
+~~~~~~~~~~~~~~~~~
+
+* Remove dependency on ``attrs`` (:issue:`178`, :issue:`179`)
+
+* Use appropriate fallbacks for CPython ABI tag (:issue:`181`, :issue:`185`)
+
+* Add manylinux2014 support (:issue:`186`)
+
+* Improve ABI detection (:issue:`181`)
+
+* Properly handle debug wheels for Python 3.8 (:issue:`172`)
+
+* Improve detection of debug builds on Windows (:issue:`194`)
+
+19.1 - 2019-07-30
+~~~~~~~~~~~~~~~~~
+
+* Add the ``packaging.tags`` module. (:issue:`156`)
+
+* Correctly handle two-digit versions in ``python_version`` (:issue:`119`)
+
+
+19.0 - 2019-01-20
+~~~~~~~~~~~~~~~~~
+
+* Fix string representation of PEP 508 direct URL requirements with markers.
+
+* Better handling of file URLs
+
+ This allows for using ``file:///absolute/path``, which was previously
+ prevented due to the missing ``netloc``.
+
+ This allows for all file URLs that ``urlunparse`` turns back into the
+ original URL to be valid.
+
+
+18.0 - 2018-09-26
+~~~~~~~~~~~~~~~~~
+
+* Improve error messages when invalid requirements are given. (:issue:`129`)
+
+
+17.1 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions.
+
+
+17.0 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Drop support for python 2.6, 3.2, and 3.3.
+
+* Define minimal pyparsing version to 2.0.2 (:issue:`91`).
+
+* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to
+ ``Version`` and ``LegacyVersion`` (:issue:`34`).
+
+* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to
+ make it easy to determine if a release is a development release.
+
+* Add ``utils.canonicalize_version`` to canonicalize version strings or
+ ``Version`` instances (:issue:`121`).
+
+
+16.8 - 2016-10-29
+~~~~~~~~~~~~~~~~~
+
+* Fix markers that utilize ``in`` so that they render correctly.
+
+* Fix an erroneous test on Python RC releases.
+
+
+16.7 - 2016-04-23
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated ``python_implementation`` marker which was
+ an undocumented setuptools marker in addition to the newer markers.
+
+
+16.6 - 2016-03-29
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated, PEP 345 environment markers in addition to
+ the newer markers.
+
+
+16.5 - 2016-02-26
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements with whitespaces between the comma
+ separators.
+
+
+16.4 - 2016-02-22
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements like ``foo (==4)``.
+
+
+16.3 - 2016-02-21
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when
+ matching legacy requirements.
+
+
+16.2 - 2016-02-09
+~~~~~~~~~~~~~~~~~
+
+* Add a function that implements the name canonicalization from PEP 503.
+
+
+16.1 - 2016-02-07
+~~~~~~~~~~~~~~~~~
+
+* Implement requirement specifiers from PEP 508.
+
+
+16.0 - 2016-01-19
+~~~~~~~~~~~~~~~~~
+
+* Relicense so that packaging is available under *either* the Apache License,
+ Version 2.0 or a 2 Clause BSD license.
+
+* Support installation of packaging when only distutils is available.
+
+* Fix ``==`` comparison when there is a prefix and a local version in play.
+ (:issue:`41`).
+
+* Implement environment markers from PEP 508.
+
+
+15.3 - 2015-08-01
+~~~~~~~~~~~~~~~~~
+
+* Normalize post-release spellings for rev/r prefixes. :issue:`35`
+
+
+15.2 - 2015-05-13
+~~~~~~~~~~~~~~~~~
+
+* Fix an error where the arbitrary specifier (``===``) was not correctly
+ allowing pre-releases when it was being used.
+
+* Expose the specifier and version parts through properties on the
+ ``Specifier`` classes.
+
+* Allow iterating over the ``SpecifierSet`` to get access to all of the
+ ``Specifier`` instances.
+
+* Allow testing if a version is contained within a specifier via the ``in``
+ operator.
+
+
+15.1 - 2015-04-13
+~~~~~~~~~~~~~~~~~
+
+* Fix a logic error that was causing inconsistent answers about whether or not
+ a pre-release was contained within a ``SpecifierSet`` or not.
+
+
+15.0 - 2015-01-02
+~~~~~~~~~~~~~~~~~
+
+* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to
+ make it easy to determine if a release is a post release.
+
+* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make
+ it easy to get the public version without any pre or post release markers.
+
+* Support the update to PEP 440 which removed the implied ``!=V.*`` when using
+ either ``>V`` or ``<V`` and which instead special cased the handling of
+ pre-releases, post-releases, and local versions when using ``>V`` or ``<V``.
+
+
+14.5 - 2014-12-17
+~~~~~~~~~~~~~~~~~
+
+* Normalize release candidates as ``rc`` instead of ``c``.
+
+* Expose the ``VERSION_PATTERN`` constant, a regular expression matching
+ a valid version.
+
+
+14.4 - 2014-12-15
+~~~~~~~~~~~~~~~~~
+
+* Ensure that versions are normalized before comparison when used in a
+ specifier with a less than (``<``) or greater than (``>``) operator.
+
+
+14.3 - 2014-11-19
+~~~~~~~~~~~~~~~~~
+
+* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely
+ handle legacy specifiers as well as PEP 440 specifiers.
+
+* **BACKWARDS INCOMPATIBLE** Move the specifier support out of
+ ``packaging.version`` into ``packaging.specifiers``.
+
+
+14.2 - 2014-09-10
+~~~~~~~~~~~~~~~~~
+
+* Add prerelease support to ``Specifier``.
+* Remove the ability to do ``item in Specifier()`` and replace it with
+ ``Specifier().contains(item)`` in order to allow flags that signal if a
+ prerelease should be accepted or not.
+* Add a method ``Specifier().filter()`` which will take an iterable and returns
+ an iterable with items that do not match the specifier filtered out.
+
+
+14.1 - 2014-09-08
+~~~~~~~~~~~~~~~~~
+
+* Allow ``LegacyVersion`` and ``Version`` to be sorted together.
+* Add ``packaging.version.parse()`` to enable easily parsing a version string
+ as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440
+ validity.
+
+
+14.0 - 2014-09-05
+~~~~~~~~~~~~~~~~~
+
+* Initial release.
+
+
+.. _`master`: https://github.com/pypa/packaging/
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..d9d70ec
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,23 @@
+Contributing to packaging
+=========================
+
+As an open source project, packaging welcomes contributions of many forms.
+
+Examples of contributions include:
+
+* Code patches
+* Documentation improvements
+* Bug reports and patch reviews
+
+Extensive contribution guidelines are available in the repository at
+``docs/development/index.rst``, or online at:
+
+https://packaging.pypa.io/en/latest/development/
+
+Security issues
+---------------
+
+To report a security issue, please follow the special `security reporting
+guidelines`_, do not report them in the public issue tracker.
+
+.. _`security reporting guidelines`: https://packaging.pypa.io/en/latest/security/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6f62d44
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,3 @@
+This software is made available under the terms of *either* of the licenses
+found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
+under the terms of *both* these licenses.
diff --git a/LICENSE.APACHE b/LICENSE.APACHE
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/LICENSE.APACHE
@@ -0,0 +1,177 @@
+
+ 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/LICENSE.BSD b/LICENSE.BSD
new file mode 100644
index 0000000..42ce7b7
--- /dev/null
+++ b/LICENSE.BSD
@@ -0,0 +1,23 @@
+Copyright (c) Donald Stufft and individual contributors.
+All rights reserved.
+
+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/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..a078133
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,24 @@
+include CHANGELOG.rst CONTRIBUTING.rst README.rst
+include LICENSE LICENSE.APACHE LICENSE.BSD
+
+include .coveragerc
+include .flake8
+include .pre-commit-config.yaml
+include mypy.ini
+
+recursive-include docs *
+recursive-include tests *.py
+recursive-include tests/manylinux hello-world-*
+recursive-include tests/musllinux glibc-*
+recursive-include tests/musllinux musl-*
+
+exclude noxfile.py
+exclude .readthedocs.yml
+exclude .travis.yml
+exclude dev-requirements.txt
+exclude tests/manylinux/build-hello-world.sh
+exclude tests/musllinux/build.sh
+exclude tests/hello-world.c
+
+prune docs/_build
+prune tasks
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..66c671b
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,452 @@
+Metadata-Version: 2.1
+Name: packaging
+Version: 21.3
+Summary: Core utilities for Python packages
+Home-page: https://github.com/pypa/packaging
+Author: Donald Stufft and individual contributors
+Author-email: donald@stufft.io
+License: BSD-2-Clause or Apache-2.0
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Requires-Python: >=3.6
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+License-File: LICENSE.APACHE
+License-File: LICENSE.BSD
+
+packaging
+=========
+
+.. start-intro
+
+Reusable core utilities for various Python Packaging
+`interoperability specifications <https://packaging.python.org/specifications/>`_.
+
+This library provides utilities that implement the interoperability
+specifications which have clearly one correct behaviour (eg: :pep:`440`)
+or benefit greatly from having a single shared implementation (eg: :pep:`425`).
+
+.. end-intro
+
+The ``packaging`` project includes the following: version handling, specifiers,
+markers, requirements, tags, utilities.
+
+Documentation
+-------------
+
+The `documentation`_ provides information and the API for the following:
+
+- Version Handling
+- Specifiers
+- Markers
+- Requirements
+- Tags
+- Utilities
+
+Installation
+------------
+
+Use ``pip`` to install these utilities::
+
+ pip install packaging
+
+Discussion
+----------
+
+If you run into bugs, you can file them in our `issue tracker`_.
+
+You can also join ``#pypa`` on Freenode to ask questions or get involved.
+
+
+.. _`documentation`: https://packaging.pypa.io/
+.. _`issue tracker`: https://github.com/pypa/packaging/issues
+
+
+Code of Conduct
+---------------
+
+Everyone interacting in the packaging project's codebases, issue trackers, chat
+rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
+
+.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+Contributing
+------------
+
+The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
+well as how to report a potential security issue. The documentation for this
+project also covers information about `project development`_ and `security`_.
+
+.. _`project development`: https://packaging.pypa.io/en/latest/development/
+.. _`security`: https://packaging.pypa.io/en/latest/security/
+
+Project History
+---------------
+
+Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
+recent changes and project history.
+
+.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
+
+Changelog
+---------
+
+21.3 - 2021-11-17
+~~~~~~~~~~~~~~~~~
+
+* Add a ``pp3-none-any`` tag (`#311 <https://github.com/pypa/packaging/issues/311>`__)
+* Replace the blank pyparsing 3 exclusion with a 3.0.5 exclusion (`#481 <https://github.com/pypa/packaging/issues/481>`__, `#486 <https://github.com/pypa/packaging/issues/486>`__)
+* Fix a spelling mistake (`#479 <https://github.com/pypa/packaging/issues/479>`__)
+
+21.2 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update documentation entry for 21.1.
+
+21.1 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update pin to pyparsing to exclude 3.0.0.
+
+21.0 - 2021-07-03
+~~~~~~~~~~~~~~~~~
+
+* PEP 656: musllinux support (`#411 <https://github.com/pypa/packaging/issues/411>`__)
+* Drop support for Python 2.7, Python 3.4 and Python 3.5.
+* Replace distutils usage with sysconfig (`#396 <https://github.com/pypa/packaging/issues/396>`__)
+* Add support for zip files in ``parse_sdist_filename`` (`#429 <https://github.com/pypa/packaging/issues/429>`__)
+* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 <https://github.com/pypa/packaging/issues/417>`__)
+* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 <https://github.com/pypa/packaging/issues/437>`__)
+* Proper keyword-only "warn" argument in packaging.tags (`#403 <https://github.com/pypa/packaging/issues/403>`__)
+* Correctly remove prerelease suffixes from ~= check (`#366 <https://github.com/pypa/packaging/issues/366>`__)
+* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 <https://github.com/pypa/packaging/issues/393>`__)
+* Use typing alias ``UnparsedVersion`` (`#398 <https://github.com/pypa/packaging/issues/398>`__)
+* Improve type inference for ``packaging.specifiers.filter()`` (`#430 <https://github.com/pypa/packaging/issues/430>`__)
+* Tighten the return type of ``canonicalize_version()`` (`#402 <https://github.com/pypa/packaging/issues/402>`__)
+
+20.9 - 2021-01-29
+~~~~~~~~~~~~~~~~~
+
+* Run `isort <https://pypi.org/project/isort/>`_ over the code base (`#377 <https://github.com/pypa/packaging/issues/377>`__)
+* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 <https://github.com/pypa/packaging/issues/379>`__)
+* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()``
+ (`#387 <https://github.com/pypa/packaging/issues/387>`__ and `#389 <https://github.com/pypa/packaging/issues/389>`__)
+
+20.8 - 2020-12-11
+~~~~~~~~~~~~~~~~~
+
+* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 <https://github.com/pypa/packaging/issues/363>`__)
+* Do not insert an underscore in wheel tags when the interpreter version number
+ is more than 2 digits (`#372 <https://github.com/pypa/packaging/issues/372>`__)
+
+20.7 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+No unreleased changes.
+
+20.6 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+.. note:: This release was subsequently yanked, and these changes were included in 20.7.
+
+* Fix flit configuration, to include LICENSE files (`#357 <https://github.com/pypa/packaging/issues/357>`__)
+* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 <https://github.com/pypa/packaging/issues/361>`__)
+* Add some missing type hints to `packaging.requirements` (issue:`350`)
+
+20.5 - 2020-11-27
+~~~~~~~~~~~~~~~~~
+
+* Officially support Python 3.9 (`#343 <https://github.com/pypa/packaging/issues/343>`__)
+* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 <https://github.com/pypa/packaging/issues/321>`__)
+* Handle ``OSError`` on non-dynamic executables when attempting to resolve
+ the glibc version string.
+
+20.4 - 2020-05-19
+~~~~~~~~~~~~~~~~~
+
+* Canonicalize version before comparing specifiers. (`#282 <https://github.com/pypa/packaging/issues/282>`__)
+* Change type hint for ``canonicalize_name`` to return
+ ``packaging.utils.NormalizedName``.
+ This enables the use of static typing tools (like mypy) to detect mixing of
+ normalized and un-normalized names.
+
+20.3 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix changelog for 20.2.
+
+20.2 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8,
+ aarch64), to report the wrong bitness.
+
+20.1 - 2020-01-24
+~~~~~~~~~~~~~~~~~~~
+
+* Fix a bug caused by reuse of an exhausted iterator. (`#257 <https://github.com/pypa/packaging/issues/257>`__)
+
+20.0 - 2020-01-06
+~~~~~~~~~~~~~~~~~
+
+* Add type hints (`#191 <https://github.com/pypa/packaging/issues/191>`__)
+
+* Add proper trove classifiers for PyPy support (`#198 <https://github.com/pypa/packaging/issues/198>`__)
+
+* Scale back depending on ``ctypes`` for manylinux support detection (`#171 <https://github.com/pypa/packaging/issues/171>`__)
+
+* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 <https://github.com/pypa/packaging/issues/193>`__)
+
+* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 <https://github.com/pypa/packaging/issues/187>`__)
+
+* Officially support Python 3.8 (`#232 <https://github.com/pypa/packaging/issues/232>`__)
+
+* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 <https://github.com/pypa/packaging/issues/226>`__)
+
+* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 <https://github.com/pypa/packaging/issues/226>`__)
+
+19.2 - 2019-09-18
+~~~~~~~~~~~~~~~~~
+
+* Remove dependency on ``attrs`` (`#178 <https://github.com/pypa/packaging/issues/178>`__, `#179 <https://github.com/pypa/packaging/issues/179>`__)
+
+* Use appropriate fallbacks for CPython ABI tag (`#181 <https://github.com/pypa/packaging/issues/181>`__, `#185 <https://github.com/pypa/packaging/issues/185>`__)
+
+* Add manylinux2014 support (`#186 <https://github.com/pypa/packaging/issues/186>`__)
+
+* Improve ABI detection (`#181 <https://github.com/pypa/packaging/issues/181>`__)
+
+* Properly handle debug wheels for Python 3.8 (`#172 <https://github.com/pypa/packaging/issues/172>`__)
+
+* Improve detection of debug builds on Windows (`#194 <https://github.com/pypa/packaging/issues/194>`__)
+
+19.1 - 2019-07-30
+~~~~~~~~~~~~~~~~~
+
+* Add the ``packaging.tags`` module. (`#156 <https://github.com/pypa/packaging/issues/156>`__)
+
+* Correctly handle two-digit versions in ``python_version`` (`#119 <https://github.com/pypa/packaging/issues/119>`__)
+
+
+19.0 - 2019-01-20
+~~~~~~~~~~~~~~~~~
+
+* Fix string representation of PEP 508 direct URL requirements with markers.
+
+* Better handling of file URLs
+
+ This allows for using ``file:///absolute/path``, which was previously
+ prevented due to the missing ``netloc``.
+
+ This allows for all file URLs that ``urlunparse`` turns back into the
+ original URL to be valid.
+
+
+18.0 - 2018-09-26
+~~~~~~~~~~~~~~~~~
+
+* Improve error messages when invalid requirements are given. (`#129 <https://github.com/pypa/packaging/issues/129>`__)
+
+
+17.1 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions.
+
+
+17.0 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Drop support for python 2.6, 3.2, and 3.3.
+
+* Define minimal pyparsing version to 2.0.2 (`#91 <https://github.com/pypa/packaging/issues/91>`__).
+
+* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to
+ ``Version`` and ``LegacyVersion`` (`#34 <https://github.com/pypa/packaging/issues/34>`__).
+
+* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to
+ make it easy to determine if a release is a development release.
+
+* Add ``utils.canonicalize_version`` to canonicalize version strings or
+ ``Version`` instances (`#121 <https://github.com/pypa/packaging/issues/121>`__).
+
+
+16.8 - 2016-10-29
+~~~~~~~~~~~~~~~~~
+
+* Fix markers that utilize ``in`` so that they render correctly.
+
+* Fix an erroneous test on Python RC releases.
+
+
+16.7 - 2016-04-23
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated ``python_implementation`` marker which was
+ an undocumented setuptools marker in addition to the newer markers.
+
+
+16.6 - 2016-03-29
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated, PEP 345 environment markers in addition to
+ the newer markers.
+
+
+16.5 - 2016-02-26
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements with whitespaces between the comma
+ separators.
+
+
+16.4 - 2016-02-22
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements like ``foo (==4)``.
+
+
+16.3 - 2016-02-21
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when
+ matching legacy requirements.
+
+
+16.2 - 2016-02-09
+~~~~~~~~~~~~~~~~~
+
+* Add a function that implements the name canonicalization from PEP 503.
+
+
+16.1 - 2016-02-07
+~~~~~~~~~~~~~~~~~
+
+* Implement requirement specifiers from PEP 508.
+
+
+16.0 - 2016-01-19
+~~~~~~~~~~~~~~~~~
+
+* Relicense so that packaging is available under *either* the Apache License,
+ Version 2.0 or a 2 Clause BSD license.
+
+* Support installation of packaging when only distutils is available.
+
+* Fix ``==`` comparison when there is a prefix and a local version in play.
+ (`#41 <https://github.com/pypa/packaging/issues/41>`__).
+
+* Implement environment markers from PEP 508.
+
+
+15.3 - 2015-08-01
+~~~~~~~~~~~~~~~~~
+
+* Normalize post-release spellings for rev/r prefixes. `#35 <https://github.com/pypa/packaging/issues/35>`__
+
+
+15.2 - 2015-05-13
+~~~~~~~~~~~~~~~~~
+
+* Fix an error where the arbitrary specifier (``===``) was not correctly
+ allowing pre-releases when it was being used.
+
+* Expose the specifier and version parts through properties on the
+ ``Specifier`` classes.
+
+* Allow iterating over the ``SpecifierSet`` to get access to all of the
+ ``Specifier`` instances.
+
+* Allow testing if a version is contained within a specifier via the ``in``
+ operator.
+
+
+15.1 - 2015-04-13
+~~~~~~~~~~~~~~~~~
+
+* Fix a logic error that was causing inconsistent answers about whether or not
+ a pre-release was contained within a ``SpecifierSet`` or not.
+
+
+15.0 - 2015-01-02
+~~~~~~~~~~~~~~~~~
+
+* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to
+ make it easy to determine if a release is a post release.
+
+* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make
+ it easy to get the public version without any pre or post release markers.
+
+* Support the update to PEP 440 which removed the implied ``!=V.*`` when using
+ either ``>V`` or ``<V`` and which instead special cased the handling of
+ pre-releases, post-releases, and local versions when using ``>V`` or ``<V``.
+
+
+14.5 - 2014-12-17
+~~~~~~~~~~~~~~~~~
+
+* Normalize release candidates as ``rc`` instead of ``c``.
+
+* Expose the ``VERSION_PATTERN`` constant, a regular expression matching
+ a valid version.
+
+
+14.4 - 2014-12-15
+~~~~~~~~~~~~~~~~~
+
+* Ensure that versions are normalized before comparison when used in a
+ specifier with a less than (``<``) or greater than (``>``) operator.
+
+
+14.3 - 2014-11-19
+~~~~~~~~~~~~~~~~~
+
+* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely
+ handle legacy specifiers as well as PEP 440 specifiers.
+
+* **BACKWARDS INCOMPATIBLE** Move the specifier support out of
+ ``packaging.version`` into ``packaging.specifiers``.
+
+
+14.2 - 2014-09-10
+~~~~~~~~~~~~~~~~~
+
+* Add prerelease support to ``Specifier``.
+* Remove the ability to do ``item in Specifier()`` and replace it with
+ ``Specifier().contains(item)`` in order to allow flags that signal if a
+ prerelease should be accepted or not.
+* Add a method ``Specifier().filter()`` which will take an iterable and returns
+ an iterable with items that do not match the specifier filtered out.
+
+
+14.1 - 2014-09-08
+~~~~~~~~~~~~~~~~~
+
+* Allow ``LegacyVersion`` and ``Version`` to be sorted together.
+* Add ``packaging.version.parse()`` to enable easily parsing a version string
+ as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440
+ validity.
+
+
+14.0 - 2014-09-05
+~~~~~~~~~~~~~~~~~
+
+* Initial release.
+
+
+.. _`master`: https://github.com/pypa/packaging/
+
+
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..e8bebe7
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,73 @@
+packaging
+=========
+
+.. start-intro
+
+Reusable core utilities for various Python Packaging
+`interoperability specifications <https://packaging.python.org/specifications/>`_.
+
+This library provides utilities that implement the interoperability
+specifications which have clearly one correct behaviour (eg: :pep:`440`)
+or benefit greatly from having a single shared implementation (eg: :pep:`425`).
+
+.. end-intro
+
+The ``packaging`` project includes the following: version handling, specifiers,
+markers, requirements, tags, utilities.
+
+Documentation
+-------------
+
+The `documentation`_ provides information and the API for the following:
+
+- Version Handling
+- Specifiers
+- Markers
+- Requirements
+- Tags
+- Utilities
+
+Installation
+------------
+
+Use ``pip`` to install these utilities::
+
+ pip install packaging
+
+Discussion
+----------
+
+If you run into bugs, you can file them in our `issue tracker`_.
+
+You can also join ``#pypa`` on Freenode to ask questions or get involved.
+
+
+.. _`documentation`: https://packaging.pypa.io/
+.. _`issue tracker`: https://github.com/pypa/packaging/issues
+
+
+Code of Conduct
+---------------
+
+Everyone interacting in the packaging project's codebases, issue trackers, chat
+rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
+
+.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+Contributing
+------------
+
+The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
+well as how to report a potential security issue. The documentation for this
+project also covers information about `project development`_ and `security`_.
+
+.. _`project development`: https://packaging.pypa.io/en/latest/development/
+.. _`security`: https://packaging.pypa.io/en/latest/security/
+
+Project History
+---------------
+
+Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
+recent changes and project history.
+
+.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..9d683b4
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/packaging.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/packaging.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/packaging"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/packaging"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_static/.empty b/docs/_static/.empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/_static/.empty
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..565b052
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..edd8dd5
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,111 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import os
+import sys
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath("."))
+
+# -- General configuration ----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.doctest",
+ "sphinx.ext.extlinks",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.viewcode",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix of source filenames.
+source_suffix = ".rst"
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+project = "Packaging"
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+
+base_dir = os.path.join(os.path.dirname(__file__), os.pardir)
+about = {}
+with open(os.path.join(base_dir, "packaging", "__about__.py")) as f:
+ exec(f.read(), about)
+
+version = release = about["__version__"]
+copyright = about["__copyright__"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ["_build"]
+
+extlinks = {
+ "issue": ("https://github.com/pypa/packaging/issues/%s", "#"),
+ "pull": ("https://github.com/pypa/packaging/pull/%s", "PR #"),
+}
+# -- Options for HTML output --------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+
+html_theme = "furo"
+html_title = "packaging"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "packagingdoc"
+
+
+# -- Options for LaTeX output -------------------------------------------------
+
+latex_elements = {}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual])
+latex_documents = [
+ ("index", "packaging.tex", "Packaging Documentation", "Donald Stufft", "manual")
+]
+
+# -- Options for manual page output -------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [("index", "packaging", "Packaging Documentation", ["Donald Stufft"], 1)]
+
+# -- Options for Texinfo output -----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ "index",
+ "packaging",
+ "Packaging Documentation",
+ "Donald Stufft",
+ "packaging",
+ "Core utilities for Python packages",
+ "Miscellaneous",
+ )
+]
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {"https://docs.python.org/": None}
+
+epub_theme = "epub"
diff --git a/docs/development/getting-started.rst b/docs/development/getting-started.rst
new file mode 100644
index 0000000..8bd42ac
--- /dev/null
+++ b/docs/development/getting-started.rst
@@ -0,0 +1,77 @@
+Getting started
+===============
+
+Working on packaging requires the installation of a small number of
+development dependencies. To see what dependencies are required to
+run the tests manually, please look at the ``noxfile.py`` file.
+
+Running tests
+~~~~~~~~~~~~~
+
+The packaging unit tests are found in the ``tests/`` directory and are
+designed to be run using `pytest`_. `pytest`_ will discover the tests
+automatically, so all you have to do is:
+
+.. code-block:: console
+
+ $ python -m pytest
+ ...
+ 29204 passed, 4 skipped, 1 xfailed in 83.98 seconds
+
+This runs the tests with the default Python interpreter. This also allows
+you to run select tests instead of the entire test suite.
+
+You can also verify that the tests pass on other supported Python interpreters.
+For this we use `nox`_, which will automatically create a `virtualenv`_ for
+each supported Python version and run the tests. For example:
+
+.. code-block:: console
+
+ $ nox -s tests
+ ...
+ nox > Ran multiple sessions:
+ nox > * tests-3.6: success
+ nox > * tests-3.7: success
+ nox > * tests-3.8: success
+ nox > * tests-3.9: success
+ nox > * tests-pypy3: skipped
+
+You may not have all the required Python versions installed, in which case you
+will see one or more ``InterpreterNotFound`` errors.
+
+Running linters
+~~~~~~~~~~~~~~~
+
+If you wish to run the linting rules, you may use `pre-commit`_ or run
+``nox -s lint``.
+
+.. code-block:: console
+
+ $ nox -s lint
+ ...
+ nox > Session lint was successful.
+
+Building documentation
+~~~~~~~~~~~~~~~~~~~~~~
+
+packaging documentation is stored in the ``docs/`` directory. It is
+written in `reStructured Text`_ and rendered using `Sphinx`_.
+
+Use `nox`_ to build the documentation. For example:
+
+.. code-block:: console
+
+ $ nox -s docs
+ ...
+ nox > Session docs was successful.
+
+The HTML documentation index can now be found at
+``docs/_build/html/index.html``.
+
+.. _`pytest`: https://pypi.org/project/pytest/
+.. _`nox`: https://pypi.org/project/nox/
+.. _`virtualenv`: https://pypi.org/project/virtualenv/
+.. _`pip`: https://pypi.org/project/pip/
+.. _`sphinx`: https://pypi.org/project/Sphinx/
+.. _`reStructured Text`: http://sphinx-doc.org/rest.html
+.. _`pre-commit`: https://pre-commit.com
diff --git a/docs/development/index.rst b/docs/development/index.rst
new file mode 100644
index 0000000..c0aea8a
--- /dev/null
+++ b/docs/development/index.rst
@@ -0,0 +1,19 @@
+Development
+===========
+
+As an open source project, packaging welcomes contributions of all
+forms. The sections below will help you get started.
+
+File bugs and feature requests on our issue tracker on `GitHub`_. If it is a
+bug check out `what to put in your bug report`_.
+
+.. toctree::
+ :maxdepth: 2
+
+ getting-started
+ submitting-patches
+ reviewing-patches
+ release-process
+
+.. _`GitHub`: https://github.com/pypa/packaging
+.. _`what to put in your bug report`: http://www.contribution-guide.org/#what-to-put-in-your-bug-report
diff --git a/docs/development/release-process.rst b/docs/development/release-process.rst
new file mode 100644
index 0000000..84e5bec
--- /dev/null
+++ b/docs/development/release-process.rst
@@ -0,0 +1,25 @@
+Release Process
+===============
+
+#. Checkout the current ``main`` branch.
+#. Install the latest ``nox``::
+
+ $ pip install nox
+
+#. Run the release automation with the required version number (YY.N)::
+
+ $ nox -s release -- YY.N
+
+ You will need the password for your GPG key as well as an API token for PyPI.
+
+#. Add a `release on GitHub <https://github.com/pypa/packaging/releases>`__.
+
+#. Notify the other project owners of the release.
+
+.. note::
+
+ Access needed for making the release are:
+
+ - PyPI maintainer (or owner) access to ``packaging``
+ - push directly to the ``main`` branch on the source repository
+ - push tags directly to the source repository
diff --git a/docs/development/reviewing-patches.rst b/docs/development/reviewing-patches.rst
new file mode 100644
index 0000000..c476c75
--- /dev/null
+++ b/docs/development/reviewing-patches.rst
@@ -0,0 +1,37 @@
+Reviewing and merging patches
+=============================
+
+Everyone is encouraged to review open pull requests. We only ask that you try
+and think carefully, ask questions and are `excellent to one another`_. Code
+review is our opportunity to share knowledge, design ideas and make friends.
+
+When reviewing a patch try to keep each of these concepts in mind:
+
+Architecture
+------------
+
+* Is the proposed change being made in the correct place?
+
+Intent
+------
+
+* What is the change being proposed?
+* Do we want this feature or is the bug they're fixing really a bug?
+
+Implementation
+--------------
+
+* Does the change do what the author claims?
+* Are there sufficient tests?
+* Has it been documented?
+* Will this change introduce new bugs?
+
+Grammar and style
+-----------------
+
+These are small things that are not caught by the automated style checkers.
+
+* Does a variable need a better name?
+* Should this be a keyword argument?
+
+.. _`excellent to one another`: https://speakerdeck.com/ohrite/better-code-review
diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst
new file mode 100644
index 0000000..fbdb5a4
--- /dev/null
+++ b/docs/development/submitting-patches.rst
@@ -0,0 +1,74 @@
+Submitting patches
+==================
+
+* Always make a new branch for your work.
+* Patches should be small to facilitate easier review. `Studies have shown`_
+ that review quality falls off as patch size grows. Sometimes this will result
+ in many small PRs to land a single large feature.
+* Larger changes should be discussed in a ticket before submission.
+* New features and significant bug fixes should be documented in the
+ :doc:`/changelog`.
+* You must have legal permission to distribute any code you contribute and it
+ must be available under both the BSD and Apache Software License Version 2.0
+ licenses.
+
+If you believe you've identified a security issue in packaging, please
+follow the directions on the :doc:`security page </security>`.
+
+Code
+----
+
+This project's source is auto-formatted with |black|. You can check if your
+code meets our requirements by running our linters against it with ``nox -s
+lint`` or ``pre-commit run --all-files``.
+
+`Write comments as complete sentences.`_
+
+Every code file must start with the boilerplate licensing notice:
+
+.. code-block:: python
+
+ # This file is dual licensed under the terms of the Apache License, Version
+ # 2.0, and the BSD License. See the LICENSE file in the root of this repository
+ # for complete details.
+
+Tests
+-----
+
+All code changes must be accompanied by unit tests with 100% code coverage (as
+measured by the combined metrics across our build matrix).
+
+
+Documentation
+-------------
+
+All features should be documented with prose in the ``docs`` section.
+
+When referring to a hypothetical individual (such as "a person receiving an
+encrypted message") use gender neutral pronouns (they/them/their).
+
+Docstrings are typically only used when writing abstract classes, but should
+be written like this if required:
+
+.. code-block:: python
+
+ def some_function(some_arg):
+ """
+ Does some things.
+
+ :param some_arg: Some argument.
+ """
+
+So, specifically:
+
+* Always use three double quotes.
+* Put the three double quotes on their own line.
+* No blank line at the end.
+* Use Sphinx parameter/attribute documentation `syntax`_.
+
+
+.. |black| replace:: ``black``
+.. _black: https://pypi.org/project/black/
+.. _`Write comments as complete sentences.`: https://nedbatchelder.com/blog/201401/comments_should_be_sentences.html
+.. _`syntax`: http://sphinx-doc.org/domains.html#info-field-lists
+.. _`Studies have shown`: http://www.ibm.com/developerworks/rational/library/11-proven-practices-for-peer-review/
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..aafdae8
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,38 @@
+Welcome to packaging
+====================
+
+.. include:: ../README.rst
+ :start-after: start-intro
+ :end-before: end-intro
+
+
+Installation
+------------
+
+You can install packaging with ``pip``:
+
+.. code-block:: console
+
+ $ pip install packaging
+
+
+.. toctree::
+ :maxdepth: 1
+ :caption: API Documentation
+ :hidden:
+
+ version
+ specifiers
+ markers
+ requirements
+ tags
+ utils
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Project
+ :hidden:
+
+ development/index
+ security
+ changelog
diff --git a/docs/markers.rst b/docs/markers.rst
new file mode 100644
index 0000000..ad25361
--- /dev/null
+++ b/docs/markers.rst
@@ -0,0 +1,93 @@
+Markers
+=======
+
+.. currentmodule:: packaging.markers
+
+One extra requirement of dealing with dependencies is the ability to specify
+if it is required depending on the operating system or Python version in use.
+`PEP 508`_ defines the scheme which has been implemented by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.markers import Marker, UndefinedEnvironmentName
+ >>> marker = Marker("python_version>'2'")
+ >>> marker
+ <Marker('python_version > "2"')>
+ >>> # We can evaluate the marker to see if it is satisfied
+ >>> marker.evaluate()
+ True
+ >>> # We can also override the environment
+ >>> env = {'python_version': '1.5.4'}
+ >>> marker.evaluate(environment=env)
+ False
+ >>> # Multiple markers can be ANDed
+ >>> and_marker = Marker("os_name=='a' and os_name=='b'")
+ >>> and_marker
+ <Marker('os_name == "a" and os_name == "b"')>
+ >>> # Multiple markers can be ORed
+ >>> or_marker = Marker("os_name=='a' or os_name=='b'")
+ >>> or_marker
+ <Marker('os_name == "a" or os_name == "b"')>
+ >>> # Markers can be also used with extras, to pull in dependencies if
+ >>> # a certain extra is being installed
+ >>> extra = Marker('extra == "bar"')
+ >>> # Evaluating an extra marker with no environment is an error
+ >>> try:
+ ... extra.evaluate()
+ ... except UndefinedEnvironmentName:
+ ... pass
+ >>> extra_environment = {'extra': ''}
+ >>> extra.evaluate(environment=extra_environment)
+ False
+ >>> extra_environment['extra'] = 'bar'
+ >>> extra.evaluate(environment=extra_environment)
+ True
+
+
+Reference
+---------
+
+.. class:: Marker(markers)
+
+ This class abstracts handling markers for dependencies of a project. It can
+ be passed a single marker or multiple markers that are ANDed or ORed
+ together. Each marker will be parsed according to PEP 508.
+
+ :param str markers: The string representation of a marker or markers.
+ :raises InvalidMarker: If the given ``markers`` are not parseable, then
+ this exception will be raised.
+
+ .. method:: evaluate(environment=None)
+
+ Evaluate the marker given the context of the current Python process.
+
+ :param dict environment: A dictionary containing keys and values to
+ override the detected environment.
+ :raises: UndefinedComparison: If the marker uses a PEP 440 comparison on
+ strings which are not valid PEP 440 versions.
+ :raises: UndefinedEnvironmentName: If the marker accesses a value that
+ isn't present inside of the environment
+ dictionary.
+
+.. exception:: InvalidMarker
+
+ Raised when attempting to create a :class:`Marker` with a string that
+ does not conform to PEP 508.
+
+
+.. exception:: UndefinedComparison
+
+ Raised when attempting to evaluate a :class:`Marker` with a PEP 440
+ comparison operator against values that are not valid PEP 440 versions.
+
+
+.. exception:: UndefinedEnvironmentName
+
+ Raised when attempting to evaluate a :class:`Marker` with a value that is
+ missing from the evaluation environment.
+
+
+.. _`PEP 508`: https://www.python.org/dev/peps/pep-0508/
diff --git a/docs/requirements.rst b/docs/requirements.rst
new file mode 100644
index 0000000..e7c5a85
--- /dev/null
+++ b/docs/requirements.rst
@@ -0,0 +1,89 @@
+Requirements
+============
+
+.. currentmodule:: packaging.requirements
+
+Parse a given requirements line for specifying dependencies of a Python
+project, using `PEP 508`_ which defines the scheme that has been implemented
+by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.requirements import Requirement
+ >>> simple_req = Requirement("name")
+ >>> simple_req
+ <Requirement('name')>
+ >>> simple_req.name
+ 'name'
+ >>> simple_req.url is None
+ True
+ >>> simple_req.extras
+ set()
+ >>> simple_req.specifier
+ <SpecifierSet('')>
+ >>> simple_req.marker is None
+ True
+ >>> # Requirements can be specified with extras, specifiers and markers
+ >>> req = Requirement('name[foo]>=2,<3; python_version>"2.0"')
+ >>> req.name
+ 'name'
+ >>> req.extras
+ {'foo'}
+ >>> req.specifier
+ <SpecifierSet('<3,>=2')>
+ >>> req.marker
+ <Marker('python_version > "2.0"')>
+ >>> # Requirements can also be specified with a URL, but may not specify
+ >>> # a version.
+ >>> url_req = Requirement('name @ https://github.com/pypa ;os_name=="a"')
+ >>> url_req.name
+ 'name'
+ >>> url_req.url
+ 'https://github.com/pypa'
+ >>> url_req.extras
+ set()
+ >>> url_req.marker
+ <Marker('os_name == "a"')>
+
+
+Reference
+---------
+
+.. class:: Requirement(requirement)
+
+ This class abstracts handling the details of a requirement for a project.
+ Each requirement will be parsed according to PEP 508.
+
+ :param str requirement: The string representation of a requirement.
+ :raises InvalidRequirement: If the given ``requirement`` is not parseable,
+ then this exception will be raised.
+
+ .. attribute:: name
+
+ The name of the requirement.
+
+ .. attribute:: url
+
+ The URL, if any where to download the requirement from. Can be None.
+
+ .. attribute:: extras
+
+ A set of extras that the requirement specifies.
+
+ .. attribute:: specifier
+
+ A :class:`~.SpecifierSet` of the version specified by the requirement.
+
+ .. attribute:: marker
+
+ A :class:`~.Marker` of the marker for the requirement. Can be None.
+
+.. exception:: InvalidRequirement
+
+ Raised when attempting to create a :class:`Requirement` with a string that
+ does not conform to PEP 508.
+
+.. _`PEP 508`: https://www.python.org/dev/peps/pep-0508/
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..a95ae18
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1 @@
+furo
diff --git a/docs/security.rst b/docs/security.rst
new file mode 100644
index 0000000..f7fdb00
--- /dev/null
+++ b/docs/security.rst
@@ -0,0 +1,18 @@
+Security
+========
+
+We take the security of packaging seriously. If you believe you've identified a
+security issue in it, DO NOT report the issue in any public forum, including
+(but not limited to):
+
+- GitHub issue tracker
+- Official or unofficial chat channels
+- Official or unofficial mailing lists
+
+Please report your issue to ``security@python.org``. Messages may be optionally
+encrypted with GPG using key fingerprints available at the `Python Security
+page <https://www.python.org/news/security/>`_.
+
+Once you've submitted an issue via email, you should receive an acknowledgment
+within 48 hours, and depending on the action to be taken, you may receive
+further follow-up emails.
diff --git a/docs/specifiers.rst b/docs/specifiers.rst
new file mode 100644
index 0000000..83299a8
--- /dev/null
+++ b/docs/specifiers.rst
@@ -0,0 +1,222 @@
+Specifiers
+==========
+
+.. currentmodule:: packaging.specifiers
+
+A core requirement of dealing with dependencies is the ability to specify what
+versions of a dependency are acceptable for you. `PEP 440`_ defines the
+standard specifier scheme which has been implemented by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.specifiers import SpecifierSet
+ >>> from packaging.version import Version
+ >>> spec1 = SpecifierSet("~=1.0")
+ >>> spec1
+ <SpecifierSet('~=1.0')>
+ >>> spec2 = SpecifierSet(">=1.0")
+ >>> spec2
+ <SpecifierSet('>=1.0')>
+ >>> # We can combine specifiers
+ >>> combined_spec = spec1 & spec2
+ >>> combined_spec
+ <SpecifierSet('>=1.0,~=1.0')>
+ >>> # We can also implicitly combine a string specifier
+ >>> combined_spec &= "!=1.1"
+ >>> combined_spec
+ <SpecifierSet('!=1.1,>=1.0,~=1.0')>
+ >>> # Create a few versions to check for contains.
+ >>> v1 = Version("1.0a5")
+ >>> v2 = Version("1.0")
+ >>> # We can check a version object to see if it falls within a specifier
+ >>> v1 in combined_spec
+ False
+ >>> v2 in combined_spec
+ True
+ >>> # We can even do the same with a string based version
+ >>> "1.4" in combined_spec
+ True
+ >>> # Finally we can filter a list of versions to get only those which are
+ >>> # contained within our specifier.
+ >>> list(combined_spec.filter([v1, v2, "1.4"]))
+ [<Version('1.0')>, '1.4']
+
+
+Reference
+---------
+
+.. class:: SpecifierSet(specifiers="", prereleases=None)
+
+ This class abstracts handling specifying the dependencies of a project. It
+ can be passed a single specifier (``>=3.0``), a comma-separated list of
+ specifiers (``>=3.0,!=3.1``), or no specifier at all. Each individual
+ specifier will be attempted to be parsed as a PEP 440 specifier
+ (:class:`Specifier`) or as a legacy, setuptools style specifier
+ (deprecated :class:`LegacySpecifier`). You may combine
+ :class:`SpecifierSet` instances using the ``&`` operator
+ (``SpecifierSet(">2") & SpecifierSet("<4")``).
+
+ Both the membership tests and the combination support using raw strings
+ in place of already instantiated objects.
+
+ :param str specifiers: The string representation of a specifier or a
+ comma-separated list of specifiers which will
+ be parsed and normalized before use.
+ :param bool prereleases: This tells the SpecifierSet if it should accept
+ prerelease versions if applicable or not. The
+ default of ``None`` will autodetect it from the
+ given specifiers.
+ :raises InvalidSpecifier: If the given ``specifiers`` are not parseable
+ than this exception will be raised.
+
+ .. attribute:: prereleases
+
+ A boolean value indicating whether this :class:`SpecifierSet`
+ represents a specifier that includes a pre-release versions. This can be
+ set to either ``True`` or ``False`` to explicitly enable or disable
+ prereleases or it can be set to ``None`` (the default) to enable
+ autodetection.
+
+ .. method:: __contains__(version)
+
+ This is the more Pythonic version of :meth:`contains()`, but does
+ not allow you to override the ``prereleases`` argument. If you
+ need that, use :meth:`contains()`.
+
+ See :meth:`contains()`.
+
+ .. method:: contains(version, prereleases=None)
+
+ Determines if ``version``, which can be either a version string, a
+ :class:`Version`, or a deprecated :class:`LegacyVersion` object, is
+ contained within this set of specifiers.
+
+ This will either match or not match prereleases based on the
+ ``prereleases`` parameter. When ``prereleases`` is set to ``None``
+ (the default) it will use the ``Specifier().prereleases`` attribute to
+ determine if to allow them. Otherwise it will use the boolean value of
+ the passed in value to determine if to allow them or not.
+
+ .. method:: __len__()
+
+ Returns the number of specifiers in this specifier set.
+
+ .. method:: __iter__()
+
+ Returns an iterator over all the underlying :class:`Specifier` (or
+ deprecated :class:`LegacySpecifier`) instances in this specifier set.
+
+ .. method:: filter(iterable, prereleases=None)
+
+ Takes an iterable that can contain version strings, :class:`~.Version`,
+ and deprecated :class:`~.LegacyVersion` instances and will then filter
+ it, returning an iterable that contains only items which match the
+ rules of this specifier object.
+
+ This method is smarter than just
+ ``filter(Specifier().contains, [...])`` because it implements the rule
+ from PEP 440 where a prerelease item SHOULD be accepted if no other
+ versions match the given specifier.
+
+ The ``prereleases`` parameter functions similarly to that of the same
+ parameter in ``contains``. If the value is ``None`` (the default) then
+ it will intelligently decide if to allow prereleases based on the
+ specifier, the ``Specifier().prereleases`` value, and the PEP 440
+ rules. Otherwise it will act as a boolean which will enable or disable
+ all prerelease versions from being included.
+
+
+.. class:: Specifier(specifier, prereleases=None)
+
+ This class abstracts the handling of a single `PEP 440`_ compatible
+ specifier. It is generally not required to instantiate this manually,
+ preferring instead to work with :class:`SpecifierSet`.
+
+ :param str specifier: The string representation of a specifier which will
+ be parsed and normalized before use.
+ :param bool prereleases: This tells the specifier if it should accept
+ prerelease versions if applicable or not. The
+ default of ``None`` will autodetect it from the
+ given specifiers.
+ :raises InvalidSpecifier: If the ``specifier`` does not conform to PEP 440
+ in any way then this exception will be raised.
+
+ .. attribute:: operator
+
+ The string value of the operator part of this specifier.
+
+ .. attribute:: version
+
+ The string version of the version part of this specifier.
+
+ .. attribute:: prereleases
+
+ See :attr:`SpecifierSet.prereleases`.
+
+ .. method:: __contains__(version)
+
+ See :meth:`SpecifierSet.__contains__()`.
+
+ .. method:: contains(version, prereleases=None)
+
+ See :meth:`SpecifierSet.contains()`.
+
+ .. method:: filter(iterable, prereleases=None)
+
+ See :meth:`SpecifierSet.filter()`.
+
+
+.. class:: LegacySpecifier(specifier, prereleases=None)
+
+ .. deprecated:: 20.5
+
+ Use :class:`Specifier` instead.
+
+ This class abstracts the handling of a single legacy, setuptools style
+ specifier. It is generally not required to instantiate this manually,
+ preferring instead to work with :class:`SpecifierSet`.
+
+ :param str specifier: The string representation of a specifier which will
+ be parsed and normalized before use.
+ :param bool prereleases: This tells the specifier if it should accept
+ prerelease versions if applicable or not. The
+ default of ``None`` will autodetect it from the
+ given specifiers.
+ :raises InvalidSpecifier: If the ``specifier`` is not parseable then this
+ will be raised.
+
+ .. attribute:: operator
+
+ The string value of the operator part of this specifier.
+
+ .. attribute:: version
+
+ The string version of the version part of this specifier.
+
+ .. attribute:: prereleases
+
+ See :attr:`SpecifierSet.prereleases`.
+
+ .. method:: __contains__(version)
+
+ See :meth:`SpecifierSet.__contains__()`.
+
+ .. method:: contains(version, prereleases=None)
+
+ See :meth:`SpecifierSet.contains()`.
+
+ .. method:: filter(iterable, prereleases=None)
+
+ See :meth:`SpecifierSet.filter()`.
+
+
+.. exception:: InvalidSpecifier
+
+ Raised when attempting to create a :class:`Specifier` with a specifier
+ string that does not conform to `PEP 440`_.
+
+
+.. _`PEP 440`: https://www.python.org/dev/peps/pep-0440/
diff --git a/docs/tags.rst b/docs/tags.rst
new file mode 100644
index 0000000..ecd613b
--- /dev/null
+++ b/docs/tags.rst
@@ -0,0 +1,225 @@
+Tags
+====
+
+.. currentmodule:: packaging.tags
+
+Wheels encode the Python interpreter, ABI, and platform that they support in
+their filenames using `platform compatibility tags`_. This module provides
+support for both parsing these tags as well as discovering what tags the
+running Python interpreter supports.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.tags import Tag, sys_tags
+ >>> import sys
+ >>> looking_for = Tag("py{major}".format(major=sys.version_info.major), "none", "any")
+ >>> supported_tags = list(sys_tags())
+ >>> looking_for in supported_tags
+ True
+ >>> really_old = Tag("py1", "none", "any")
+ >>> wheels = {really_old, looking_for}
+ >>> best_wheel = None
+ >>> for supported_tag in supported_tags:
+ ... for wheel_tag in wheels:
+ ... if supported_tag == wheel_tag:
+ ... best_wheel = wheel_tag
+ ... break
+ >>> best_wheel == looking_for
+ True
+
+Reference
+---------
+
+High Level Interface
+''''''''''''''''''''
+
+The following functions are the main interface to the library, and are typically the only
+items that applications should need to reference, in order to parse and check tags.
+
+.. class:: Tag(interpreter, abi, platform)
+
+ A representation of the tag triple for a wheel. Instances are considered
+ immutable and thus are hashable. Equality checking is also supported.
+
+ :param str interpreter: The interpreter name, e.g. ``"py"``
+ (see :attr:`INTERPRETER_SHORT_NAMES` for mapping
+ well-known interpreter names to their short names).
+ :param str abi: The ABI that a wheel supports, e.g. ``"cp37m"``.
+ :param str platform: The OS/platform the wheel supports,
+ e.g. ``"win_amd64"``.
+
+ .. attribute:: interpreter
+
+ The interpreter name.
+
+ .. attribute:: abi
+
+ The supported ABI.
+
+ .. attribute:: platform
+
+ The OS/platform.
+
+
+.. function:: parse_tag(tag)
+
+ Parses the provided ``tag`` into a set of :class:`Tag` instances.
+
+ Returning a set is required due to the possibility that the tag is a
+ `compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both
+ Python 2 and Python 3.
+
+ :param str tag: The tag to parse, e.g. ``"py3-none-any"``.
+
+
+.. function:: sys_tags(*, warn=False)
+
+ Yields the tags that the running interpreter supports.
+
+ The iterable is ordered so that the best-matching tag is first in the
+ sequence. The exact preferential order to tags is interpreter-specific, but
+ in general the tag importance is in the order of:
+
+ 1. Interpreter
+ 2. Platform
+ 3. ABI
+
+ This order is due to the fact that an ABI is inherently tied to the
+ platform, but platform-specific code is not necessarily tied to the ABI. The
+ interpreter is the most important tag as it dictates basic support for any
+ wheel.
+
+ The function returns an iterable in order to allow for the possible
+ short-circuiting of tag generation if the entire sequence is not necessary
+ and tag calculation happens to be expensive.
+
+ :param bool warn: Whether warnings should be logged. Defaults to ``False``.
+
+
+Low Level Interface
+'''''''''''''''''''
+
+The following functions are low-level implementation details. They should typically not
+be needed in application code, unless the application has specialised requirements (for
+example, constructing sets of supported tags for environments other than the running
+interpreter).
+
+These functions capture the precise details of which environments support which tags. That
+information is not defined in the compatibility tag standards but is noted as being up
+to the implementation to provide.
+
+
+.. attribute:: INTERPRETER_SHORT_NAMES
+
+ A dictionary mapping interpreter names to their `abbreviation codes`_
+ (e.g. ``"cpython"`` is ``"cp"``). All interpreter names are lower-case.
+
+
+.. function:: interpreter_name()
+
+ Returns the running interpreter's name.
+
+ This typically acts as the prefix to the :attr:`~Tag.interpreter` tag.
+
+
+.. function:: interpreter_version(*, warn=False)
+
+ Returns the running interpreter's version.
+
+ This typically acts as the suffix to the :attr:`~Tag.interpreter` tag.
+
+
+.. function:: mac_platforms(version=None, arch=None)
+
+ Yields the :attr:`~Tag.platform` tags for macOS.
+
+ :param tuple version: A two-item tuple presenting the version of macOS.
+ Defaults to the current system's version.
+ :param str arch: The CPU architecture. Defaults to the architecture of the
+ current system, e.g. ``"x86_64"``.
+
+ .. note::
+ Equivalent support for the other major platforms is purposefully not
+ provided:
+
+ - On Windows, platform compatibility is statically specified
+ - On Linux, code must be run on the system itself to determine
+ compatibility
+
+
+.. function:: platform_tags(version=None, arch=None)
+
+ Yields the :attr:`~Tag.platform` tags for the running interpreter.
+
+
+.. function:: compatible_tags(python_version=None, interpreter=None, platforms=None)
+
+ Yields the tags for an interpreter compatible with the Python version
+ specified by ``python_version``.
+
+ The specific tags generated are:
+
+ - ``py*-none-<platform>``
+ - ``<interpreter>-none-any`` if ``interpreter`` is provided
+ - ``py*-none-any``
+
+ :param Sequence python_version: A one- or two-item sequence representing the
+ compatible version of Python. Defaults to
+ ``sys.version_info[:2]``.
+ :param str interpreter: The name of the interpreter (if known), e.g.
+ ``"cp38"``. Defaults to the current interpreter.
+ :param Iterable platforms: Iterable of compatible platforms. Defaults to the
+ platforms compatible with the current system.
+
+.. function:: cpython_tags(python_version=None, abis=None, platforms=None, *, warn=False)
+
+ Yields the tags for the CPython interpreter.
+
+ The specific tags generated are:
+
+ - ``cp<python_version>-<abi>-<platform>``
+ - ``cp<python_version>-abi3-<platform>``
+ - ``cp<python_version>-none-<platform>``
+ - ``cp<older version>-abi3-<platform>`` where "older version" is all older
+ minor versions down to Python 3.2 (when ``abi3`` was introduced)
+
+ If ``python_version`` only provides a major-only version then only
+ user-provided ABIs via ``abis`` and the ``none`` ABI will be used.
+
+ :param Sequence python_version: A one- or two-item sequence representing the
+ targeted Python version. Defaults to
+ ``sys.version_info[:2]``.
+ :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
+ compatible with the current system.
+ :param Iterable platforms: Iterable of compatible platforms. Defaults to the
+ platforms compatible with the current system.
+ :param bool warn: Whether warnings should be logged. Defaults to ``False``.
+
+.. function:: generic_tags(interpreter=None, abis=None, platforms=None, *, warn=False)
+
+ Yields the tags for an interpreter which requires no specialization.
+
+ This function should be used if one of the other interpreter-specific
+ functions provided by this module is not appropriate (i.e. not calculating
+ tags for a CPython interpreter).
+
+ The specific tags generated are:
+
+ - ``<interpreter>-<abi>-<platform>``
+
+ The ``"none"`` ABI will be added if it was not explicitly provided.
+
+ :param str interpreter: The name of the interpreter. Defaults to being
+ calculated.
+ :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
+ compatible with the current system.
+ :param Iterable platforms: Iterable of compatible platforms. Defaults to the
+ platforms compatible with the current system.
+ :param bool warn: Whether warnings should be logged. Defaults to ``False``.
+
+.. _`abbreviation codes`: https://www.python.org/dev/peps/pep-0425/#python-tag
+.. _`compressed tag set`: https://www.python.org/dev/peps/pep-0425/#compressed-tag-sets
+.. _`platform compatibility tags`: https://packaging.python.org/specifications/platform-compatibility-tags/
diff --git a/docs/utils.rst b/docs/utils.rst
new file mode 100644
index 0000000..8fbb025
--- /dev/null
+++ b/docs/utils.rst
@@ -0,0 +1,92 @@
+Utilities
+=========
+
+.. currentmodule:: packaging.utils
+
+
+A set of small, helper utilities for dealing with Python packages.
+
+
+Reference
+---------
+
+.. function:: canonicalize_name(name)
+
+ This function takes a valid Python package name, and returns the normalized
+ form of it.
+
+ :param str name: The name to normalize.
+
+ .. doctest::
+
+ >>> from packaging.utils import canonicalize_name
+ >>> canonicalize_name("Django")
+ 'django'
+ >>> canonicalize_name("oslo.concurrency")
+ 'oslo-concurrency'
+ >>> canonicalize_name("requests")
+ 'requests'
+
+.. function:: canonicalize_version(version)
+
+ This function takes a string representing a package version (or a
+ :class:`~packaging.version.Version` instance), and returns the
+ normalized form of it.
+
+ :param str version: The version to normalize.
+
+ .. doctest::
+
+ >>> from packaging.utils import canonicalize_version
+ >>> canonicalize_version('1.4.0.0.0')
+ '1.4'
+
+.. function:: parse_wheel_filename(filename)
+
+ This function takes the filename of a wheel file, and parses it,
+ returning a tuple of name, version, build number, and tags.
+
+ The name part of the tuple is normalized. The version portion is an
+ instance of :class:`~packaging.version.Version`. The build number
+ is ``()`` if there is no build number in the wheel filename,
+ otherwise a two-item tuple of an integer for the leading digits and
+ a string for the rest of the build number. The tags portion is an
+ instance of :class:`~packaging.tags.Tag`.
+
+ :param str filename: The name of the wheel file.
+
+ .. doctest::
+
+ >>> from packaging.utils import parse_wheel_filename
+ >>> from packaging.tags import Tag
+ >>> from packaging.version import Version
+ >>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
+ >>> name
+ 'foo'
+ >>> ver == Version('1.0')
+ True
+ >>> tags == {Tag("py3", "none", "any")}
+ True
+ >>> not build
+ True
+
+.. function:: parse_sdist_filename(filename)
+
+ This function takes the filename of a sdist file (as specified
+ in the `Source distribution format`_ documentation), and parses
+ it, returning a tuple of the normalized name and version as
+ represented by an instance of :class:`~packaging.version.Version`.
+
+ :param str filename: The name of the sdist file.
+
+ .. doctest::
+
+ >>> from packaging.utils import parse_sdist_filename
+ >>> from packaging.version import Version
+ >>> name, ver = parse_sdist_filename("foo-1.0.tar.gz")
+ >>> name
+ 'foo'
+ >>> ver == Version('1.0')
+ True
+
+.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
diff --git a/docs/version.rst b/docs/version.rst
new file mode 100644
index 0000000..a43cf78
--- /dev/null
+++ b/docs/version.rst
@@ -0,0 +1,292 @@
+Version Handling
+================
+
+.. currentmodule:: packaging.version
+
+A core requirement of dealing with packages is the ability to work with
+versions. `PEP 440`_ defines the standard version scheme for Python packages
+which has been implemented by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.version import Version, parse
+ >>> v1 = parse("1.0a5")
+ >>> v2 = Version("1.0")
+ >>> v1
+ <Version('1.0a5')>
+ >>> v2
+ <Version('1.0')>
+ >>> v1 < v2
+ True
+ >>> v1.epoch
+ 0
+ >>> v1.release
+ (1, 0)
+ >>> v1.pre
+ ('a', 5)
+ >>> v1.is_prerelease
+ True
+ >>> v2.is_prerelease
+ False
+ >>> Version("french toast")
+ Traceback (most recent call last):
+ ...
+ InvalidVersion: Invalid version: 'french toast'
+ >>> Version("1.0").post
+ >>> Version("1.0").is_postrelease
+ False
+ >>> Version("1.0.post0").post
+ 0
+ >>> Version("1.0.post0").is_postrelease
+ True
+
+
+Reference
+---------
+
+.. function:: parse(version)
+
+ This function takes a version string and will parse it as a
+ :class:`Version` if the version is a valid PEP 440 version, otherwise it
+ will parse it as a deprecated :class:`LegacyVersion`.
+
+
+.. class:: Version(version)
+
+ This class abstracts handling of a project's versions. It implements the
+ scheme defined in `PEP 440`_. A :class:`Version` instance is comparison
+ aware and can be compared and sorted using the standard Python interfaces.
+
+ :param str version: The string representation of a version which will be
+ parsed and normalized before use.
+ :raises InvalidVersion: If the ``version`` does not conform to PEP 440 in
+ any way then this exception will be raised.
+
+ .. attribute:: public
+
+ A string representing the public version portion of this ``Version()``.
+
+ .. attribute:: base_version
+
+ A string representing the base version of this :class:`Version`
+ instance. The base version is the public version of the project without
+ any pre or post release markers.
+
+ .. attribute:: epoch
+
+ An integer giving the version epoch of this :class:`Version` instance
+
+ .. attribute:: release
+
+ A tuple of integers giving the components of the release segment of
+ this :class:`Version` instance; that is, the ``1.2.3`` part of the
+ version number, including trailing zeroes but not including the epoch
+ or any prerelease/development/postrelease suffixes
+
+ .. attribute:: major
+
+ An integer representing the first item of :attr:`release` or ``0`` if unavailable.
+
+ .. attribute:: minor
+
+ An integer representing the second item of :attr:`release` or ``0`` if unavailable.
+
+ .. attribute:: micro
+
+ An integer representing the third item of :attr:`release` or ``0`` if unavailable.
+
+ .. attribute:: local
+
+ A string representing the local version portion of this ``Version()``
+ if it has one, or ``None`` otherwise.
+
+ .. attribute:: pre
+
+ If this :class:`Version` instance represents a prerelease, this
+ attribute will be a pair of the prerelease phase (the string ``"a"``,
+ ``"b"``, or ``"rc"``) and the prerelease number (an integer). If this
+ instance is not a prerelease, the attribute will be `None`.
+
+ .. attribute:: is_prerelease
+
+ A boolean value indicating whether this :class:`Version` instance
+ represents a prerelease and/or development release.
+
+ .. attribute:: dev
+
+ If this :class:`Version` instance represents a development release,
+ this attribute will be the development release number (an integer);
+ otherwise, it will be `None`.
+
+ .. attribute:: is_devrelease
+
+ A boolean value indicating whether this :class:`Version` instance
+ represents a development release.
+
+ .. attribute:: post
+
+ If this :class:`Version` instance represents a postrelease, this
+ attribute will be the postrelease number (an integer); otherwise, it
+ will be `None`.
+
+ .. attribute:: is_postrelease
+
+ A boolean value indicating whether this :class:`Version` instance
+ represents a post-release.
+
+
+.. class:: LegacyVersion(version)
+
+ .. deprecated:: 20.5
+
+ Use :class:`Version` instead.
+
+ This class abstracts handling of a project's versions if they are not
+ compatible with the scheme defined in `PEP 440`_. It implements a similar
+ interface to that of :class:`Version`.
+
+ This class implements the previous de facto sorting algorithm used by
+ setuptools, however it will always sort as less than a :class:`Version`
+ instance.
+
+ :param str version: The string representation of a version which will be
+ used as is.
+
+ .. note::
+
+ :class:`LegacyVersion` instances are always ordered lower than :class:`Version` instances.
+
+ >>> from packaging.version import Version, LegacyVersion
+ >>> v1 = Version("1.0")
+ >>> v2 = LegacyVersion("1.0")
+ >>> v1 > v2
+ True
+ >>> v3 = LegacyVersion("1.3")
+ >>> v1 > v3
+ True
+
+ Also note that some strings are still valid PEP 440 strings (:class:`Version`), even if they look very similar to
+ other versions that are not (:class:`LegacyVersion`). Examples include versions with `Pre-release spelling`_ and
+ `Post-release spelling`_.
+
+ >>> from packaging.version import parse
+ >>> v1 = parse('0.9.8a')
+ >>> v2 = parse('0.9.8beta')
+ >>> v3 = parse('0.9.8r')
+ >>> v4 = parse('0.9.8rev')
+ >>> v5 = parse('0.9.8t')
+ >>> v1
+ <Version('0.9.8a0')>
+ >>> v1.is_prerelease
+ True
+ >>> v2
+ <Version('0.9.8b0')>
+ >>> v2.is_prerelease
+ True
+ >>> v3
+ <Version('0.9.8.post0')>
+ >>> v3.is_postrelease
+ True
+ >>> v4
+ <Version('0.9.8.post0')>
+ >>> v4.is_postrelease
+ True
+ >>> v5
+ <LegacyVersion('0.9.8t')>
+ >>> v5.is_prerelease
+ False
+ >>> v5.is_postrelease
+ False
+
+ .. attribute:: public
+
+ A string representing the public version portion of this
+ :class:`LegacyVersion`. This will always be the entire version string.
+
+ .. attribute:: base_version
+
+ A string representing the base version portion of this
+ :class:`LegacyVersion` instance. This will always be the entire version
+ string.
+
+ .. attribute:: epoch
+
+ This will always be ``-1`` since without `PEP 440`_ we do not have the
+ concept of version epochs. The value reflects the fact that
+ :class:`LegacyVersion` instances always compare less than
+ :class:`Version` instances.
+
+ .. attribute:: release
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a release segment or its components. It exists
+ primarily to allow a :class:`LegacyVersion` to be used as a stand in
+ for a :class:`Version`.
+
+ .. attribute:: local
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a local version. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: pre
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a prerelease. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: is_prerelease
+
+ A boolean value indicating whether this :class:`LegacyVersion`
+ represents a prerelease and/or development release. Since without
+ `PEP 440`_ there is no concept of pre or dev releases this will
+ always be `False` and exists for compatibility with :class:`Version`.
+
+ .. attribute:: dev
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a development release. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: is_devrelease
+
+ A boolean value indicating whether this :class:`LegacyVersion`
+ represents a development release. Since without `PEP 440`_ there is
+ no concept of dev releases this will always be `False` and exists for
+ compatibility with :class:`Version`.
+
+ .. attribute:: post
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a postrelease. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: is_postrelease
+
+ A boolean value indicating whether this :class:`LegacyVersion`
+ represents a post-release. Since without `PEP 440`_ there is no concept
+ of post-releases this will always be ``False`` and exists for
+ compatibility with :class:`Version`.
+
+
+.. exception:: InvalidVersion
+
+ Raised when attempting to create a :class:`Version` with a version string
+ that does not conform to `PEP 440`_.
+
+
+.. data:: VERSION_PATTERN
+
+ A string containing the regular expression used to match a valid version.
+ The pattern is not anchored at either end, and is intended for embedding
+ in larger expressions (for example, matching a version number as part of
+ a file name). The regular expression should be compiled with the
+ ``re.VERBOSE`` and ``re.IGNORECASE`` flags set.
+
+
+.. _PEP 440: https://www.python.org/dev/peps/pep-0440/
+.. _Pre-release spelling : https://www.python.org/dev/peps/pep-0440/#pre-release-spelling
+.. _Post-release spelling : https://www.python.org/dev/peps/pep-0440/#post-release-spelling
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..d88ab8f
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,17 @@
+[mypy]
+ignore_missing_imports = True
+
+# The following are the flags enabled by --strict
+warn_unused_configs = True
+disallow_subclassing_any = True
+disallow_any_generics = True
+disallow_untyped_calls = True
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+check_untyped_defs = True
+disallow_untyped_decorators = True
+no_implicit_optional = True
+warn_redundant_casts = True
+warn_unused_ignores = True
+warn_return_any = True
+no_implicit_reexport = True
diff --git a/packaging.egg-info/PKG-INFO b/packaging.egg-info/PKG-INFO
new file mode 100644
index 0000000..66c671b
--- /dev/null
+++ b/packaging.egg-info/PKG-INFO
@@ -0,0 +1,452 @@
+Metadata-Version: 2.1
+Name: packaging
+Version: 21.3
+Summary: Core utilities for Python packages
+Home-page: https://github.com/pypa/packaging
+Author: Donald Stufft and individual contributors
+Author-email: donald@stufft.io
+License: BSD-2-Clause or Apache-2.0
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Requires-Python: >=3.6
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+License-File: LICENSE.APACHE
+License-File: LICENSE.BSD
+
+packaging
+=========
+
+.. start-intro
+
+Reusable core utilities for various Python Packaging
+`interoperability specifications <https://packaging.python.org/specifications/>`_.
+
+This library provides utilities that implement the interoperability
+specifications which have clearly one correct behaviour (eg: :pep:`440`)
+or benefit greatly from having a single shared implementation (eg: :pep:`425`).
+
+.. end-intro
+
+The ``packaging`` project includes the following: version handling, specifiers,
+markers, requirements, tags, utilities.
+
+Documentation
+-------------
+
+The `documentation`_ provides information and the API for the following:
+
+- Version Handling
+- Specifiers
+- Markers
+- Requirements
+- Tags
+- Utilities
+
+Installation
+------------
+
+Use ``pip`` to install these utilities::
+
+ pip install packaging
+
+Discussion
+----------
+
+If you run into bugs, you can file them in our `issue tracker`_.
+
+You can also join ``#pypa`` on Freenode to ask questions or get involved.
+
+
+.. _`documentation`: https://packaging.pypa.io/
+.. _`issue tracker`: https://github.com/pypa/packaging/issues
+
+
+Code of Conduct
+---------------
+
+Everyone interacting in the packaging project's codebases, issue trackers, chat
+rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
+
+.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+Contributing
+------------
+
+The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
+well as how to report a potential security issue. The documentation for this
+project also covers information about `project development`_ and `security`_.
+
+.. _`project development`: https://packaging.pypa.io/en/latest/development/
+.. _`security`: https://packaging.pypa.io/en/latest/security/
+
+Project History
+---------------
+
+Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
+recent changes and project history.
+
+.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
+
+Changelog
+---------
+
+21.3 - 2021-11-17
+~~~~~~~~~~~~~~~~~
+
+* Add a ``pp3-none-any`` tag (`#311 <https://github.com/pypa/packaging/issues/311>`__)
+* Replace the blank pyparsing 3 exclusion with a 3.0.5 exclusion (`#481 <https://github.com/pypa/packaging/issues/481>`__, `#486 <https://github.com/pypa/packaging/issues/486>`__)
+* Fix a spelling mistake (`#479 <https://github.com/pypa/packaging/issues/479>`__)
+
+21.2 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update documentation entry for 21.1.
+
+21.1 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update pin to pyparsing to exclude 3.0.0.
+
+21.0 - 2021-07-03
+~~~~~~~~~~~~~~~~~
+
+* PEP 656: musllinux support (`#411 <https://github.com/pypa/packaging/issues/411>`__)
+* Drop support for Python 2.7, Python 3.4 and Python 3.5.
+* Replace distutils usage with sysconfig (`#396 <https://github.com/pypa/packaging/issues/396>`__)
+* Add support for zip files in ``parse_sdist_filename`` (`#429 <https://github.com/pypa/packaging/issues/429>`__)
+* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 <https://github.com/pypa/packaging/issues/417>`__)
+* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 <https://github.com/pypa/packaging/issues/437>`__)
+* Proper keyword-only "warn" argument in packaging.tags (`#403 <https://github.com/pypa/packaging/issues/403>`__)
+* Correctly remove prerelease suffixes from ~= check (`#366 <https://github.com/pypa/packaging/issues/366>`__)
+* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 <https://github.com/pypa/packaging/issues/393>`__)
+* Use typing alias ``UnparsedVersion`` (`#398 <https://github.com/pypa/packaging/issues/398>`__)
+* Improve type inference for ``packaging.specifiers.filter()`` (`#430 <https://github.com/pypa/packaging/issues/430>`__)
+* Tighten the return type of ``canonicalize_version()`` (`#402 <https://github.com/pypa/packaging/issues/402>`__)
+
+20.9 - 2021-01-29
+~~~~~~~~~~~~~~~~~
+
+* Run `isort <https://pypi.org/project/isort/>`_ over the code base (`#377 <https://github.com/pypa/packaging/issues/377>`__)
+* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 <https://github.com/pypa/packaging/issues/379>`__)
+* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()``
+ (`#387 <https://github.com/pypa/packaging/issues/387>`__ and `#389 <https://github.com/pypa/packaging/issues/389>`__)
+
+20.8 - 2020-12-11
+~~~~~~~~~~~~~~~~~
+
+* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 <https://github.com/pypa/packaging/issues/363>`__)
+* Do not insert an underscore in wheel tags when the interpreter version number
+ is more than 2 digits (`#372 <https://github.com/pypa/packaging/issues/372>`__)
+
+20.7 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+No unreleased changes.
+
+20.6 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+.. note:: This release was subsequently yanked, and these changes were included in 20.7.
+
+* Fix flit configuration, to include LICENSE files (`#357 <https://github.com/pypa/packaging/issues/357>`__)
+* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 <https://github.com/pypa/packaging/issues/361>`__)
+* Add some missing type hints to `packaging.requirements` (issue:`350`)
+
+20.5 - 2020-11-27
+~~~~~~~~~~~~~~~~~
+
+* Officially support Python 3.9 (`#343 <https://github.com/pypa/packaging/issues/343>`__)
+* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 <https://github.com/pypa/packaging/issues/321>`__)
+* Handle ``OSError`` on non-dynamic executables when attempting to resolve
+ the glibc version string.
+
+20.4 - 2020-05-19
+~~~~~~~~~~~~~~~~~
+
+* Canonicalize version before comparing specifiers. (`#282 <https://github.com/pypa/packaging/issues/282>`__)
+* Change type hint for ``canonicalize_name`` to return
+ ``packaging.utils.NormalizedName``.
+ This enables the use of static typing tools (like mypy) to detect mixing of
+ normalized and un-normalized names.
+
+20.3 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix changelog for 20.2.
+
+20.2 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8,
+ aarch64), to report the wrong bitness.
+
+20.1 - 2020-01-24
+~~~~~~~~~~~~~~~~~~~
+
+* Fix a bug caused by reuse of an exhausted iterator. (`#257 <https://github.com/pypa/packaging/issues/257>`__)
+
+20.0 - 2020-01-06
+~~~~~~~~~~~~~~~~~
+
+* Add type hints (`#191 <https://github.com/pypa/packaging/issues/191>`__)
+
+* Add proper trove classifiers for PyPy support (`#198 <https://github.com/pypa/packaging/issues/198>`__)
+
+* Scale back depending on ``ctypes`` for manylinux support detection (`#171 <https://github.com/pypa/packaging/issues/171>`__)
+
+* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 <https://github.com/pypa/packaging/issues/193>`__)
+
+* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 <https://github.com/pypa/packaging/issues/187>`__)
+
+* Officially support Python 3.8 (`#232 <https://github.com/pypa/packaging/issues/232>`__)
+
+* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 <https://github.com/pypa/packaging/issues/226>`__)
+
+* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 <https://github.com/pypa/packaging/issues/226>`__)
+
+19.2 - 2019-09-18
+~~~~~~~~~~~~~~~~~
+
+* Remove dependency on ``attrs`` (`#178 <https://github.com/pypa/packaging/issues/178>`__, `#179 <https://github.com/pypa/packaging/issues/179>`__)
+
+* Use appropriate fallbacks for CPython ABI tag (`#181 <https://github.com/pypa/packaging/issues/181>`__, `#185 <https://github.com/pypa/packaging/issues/185>`__)
+
+* Add manylinux2014 support (`#186 <https://github.com/pypa/packaging/issues/186>`__)
+
+* Improve ABI detection (`#181 <https://github.com/pypa/packaging/issues/181>`__)
+
+* Properly handle debug wheels for Python 3.8 (`#172 <https://github.com/pypa/packaging/issues/172>`__)
+
+* Improve detection of debug builds on Windows (`#194 <https://github.com/pypa/packaging/issues/194>`__)
+
+19.1 - 2019-07-30
+~~~~~~~~~~~~~~~~~
+
+* Add the ``packaging.tags`` module. (`#156 <https://github.com/pypa/packaging/issues/156>`__)
+
+* Correctly handle two-digit versions in ``python_version`` (`#119 <https://github.com/pypa/packaging/issues/119>`__)
+
+
+19.0 - 2019-01-20
+~~~~~~~~~~~~~~~~~
+
+* Fix string representation of PEP 508 direct URL requirements with markers.
+
+* Better handling of file URLs
+
+ This allows for using ``file:///absolute/path``, which was previously
+ prevented due to the missing ``netloc``.
+
+ This allows for all file URLs that ``urlunparse`` turns back into the
+ original URL to be valid.
+
+
+18.0 - 2018-09-26
+~~~~~~~~~~~~~~~~~
+
+* Improve error messages when invalid requirements are given. (`#129 <https://github.com/pypa/packaging/issues/129>`__)
+
+
+17.1 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions.
+
+
+17.0 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Drop support for python 2.6, 3.2, and 3.3.
+
+* Define minimal pyparsing version to 2.0.2 (`#91 <https://github.com/pypa/packaging/issues/91>`__).
+
+* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to
+ ``Version`` and ``LegacyVersion`` (`#34 <https://github.com/pypa/packaging/issues/34>`__).
+
+* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to
+ make it easy to determine if a release is a development release.
+
+* Add ``utils.canonicalize_version`` to canonicalize version strings or
+ ``Version`` instances (`#121 <https://github.com/pypa/packaging/issues/121>`__).
+
+
+16.8 - 2016-10-29
+~~~~~~~~~~~~~~~~~
+
+* Fix markers that utilize ``in`` so that they render correctly.
+
+* Fix an erroneous test on Python RC releases.
+
+
+16.7 - 2016-04-23
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated ``python_implementation`` marker which was
+ an undocumented setuptools marker in addition to the newer markers.
+
+
+16.6 - 2016-03-29
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated, PEP 345 environment markers in addition to
+ the newer markers.
+
+
+16.5 - 2016-02-26
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements with whitespaces between the comma
+ separators.
+
+
+16.4 - 2016-02-22
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements like ``foo (==4)``.
+
+
+16.3 - 2016-02-21
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when
+ matching legacy requirements.
+
+
+16.2 - 2016-02-09
+~~~~~~~~~~~~~~~~~
+
+* Add a function that implements the name canonicalization from PEP 503.
+
+
+16.1 - 2016-02-07
+~~~~~~~~~~~~~~~~~
+
+* Implement requirement specifiers from PEP 508.
+
+
+16.0 - 2016-01-19
+~~~~~~~~~~~~~~~~~
+
+* Relicense so that packaging is available under *either* the Apache License,
+ Version 2.0 or a 2 Clause BSD license.
+
+* Support installation of packaging when only distutils is available.
+
+* Fix ``==`` comparison when there is a prefix and a local version in play.
+ (`#41 <https://github.com/pypa/packaging/issues/41>`__).
+
+* Implement environment markers from PEP 508.
+
+
+15.3 - 2015-08-01
+~~~~~~~~~~~~~~~~~
+
+* Normalize post-release spellings for rev/r prefixes. `#35 <https://github.com/pypa/packaging/issues/35>`__
+
+
+15.2 - 2015-05-13
+~~~~~~~~~~~~~~~~~
+
+* Fix an error where the arbitrary specifier (``===``) was not correctly
+ allowing pre-releases when it was being used.
+
+* Expose the specifier and version parts through properties on the
+ ``Specifier`` classes.
+
+* Allow iterating over the ``SpecifierSet`` to get access to all of the
+ ``Specifier`` instances.
+
+* Allow testing if a version is contained within a specifier via the ``in``
+ operator.
+
+
+15.1 - 2015-04-13
+~~~~~~~~~~~~~~~~~
+
+* Fix a logic error that was causing inconsistent answers about whether or not
+ a pre-release was contained within a ``SpecifierSet`` or not.
+
+
+15.0 - 2015-01-02
+~~~~~~~~~~~~~~~~~
+
+* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to
+ make it easy to determine if a release is a post release.
+
+* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make
+ it easy to get the public version without any pre or post release markers.
+
+* Support the update to PEP 440 which removed the implied ``!=V.*`` when using
+ either ``>V`` or ``<V`` and which instead special cased the handling of
+ pre-releases, post-releases, and local versions when using ``>V`` or ``<V``.
+
+
+14.5 - 2014-12-17
+~~~~~~~~~~~~~~~~~
+
+* Normalize release candidates as ``rc`` instead of ``c``.
+
+* Expose the ``VERSION_PATTERN`` constant, a regular expression matching
+ a valid version.
+
+
+14.4 - 2014-12-15
+~~~~~~~~~~~~~~~~~
+
+* Ensure that versions are normalized before comparison when used in a
+ specifier with a less than (``<``) or greater than (``>``) operator.
+
+
+14.3 - 2014-11-19
+~~~~~~~~~~~~~~~~~
+
+* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely
+ handle legacy specifiers as well as PEP 440 specifiers.
+
+* **BACKWARDS INCOMPATIBLE** Move the specifier support out of
+ ``packaging.version`` into ``packaging.specifiers``.
+
+
+14.2 - 2014-09-10
+~~~~~~~~~~~~~~~~~
+
+* Add prerelease support to ``Specifier``.
+* Remove the ability to do ``item in Specifier()`` and replace it with
+ ``Specifier().contains(item)`` in order to allow flags that signal if a
+ prerelease should be accepted or not.
+* Add a method ``Specifier().filter()`` which will take an iterable and returns
+ an iterable with items that do not match the specifier filtered out.
+
+
+14.1 - 2014-09-08
+~~~~~~~~~~~~~~~~~
+
+* Allow ``LegacyVersion`` and ``Version`` to be sorted together.
+* Add ``packaging.version.parse()`` to enable easily parsing a version string
+ as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440
+ validity.
+
+
+14.0 - 2014-09-05
+~~~~~~~~~~~~~~~~~
+
+* Initial release.
+
+
+.. _`master`: https://github.com/pypa/packaging/
+
+
diff --git a/packaging.egg-info/SOURCES.txt b/packaging.egg-info/SOURCES.txt
new file mode 100644
index 0000000..645c647
--- /dev/null
+++ b/packaging.egg-info/SOURCES.txt
@@ -0,0 +1,73 @@
+.coveragerc
+.flake8
+.pre-commit-config.yaml
+CHANGELOG.rst
+CONTRIBUTING.rst
+LICENSE
+LICENSE.APACHE
+LICENSE.BSD
+MANIFEST.in
+README.rst
+mypy.ini
+pyproject.toml
+setup.cfg
+setup.py
+docs/Makefile
+docs/changelog.rst
+docs/conf.py
+docs/index.rst
+docs/markers.rst
+docs/requirements.rst
+docs/requirements.txt
+docs/security.rst
+docs/specifiers.rst
+docs/tags.rst
+docs/utils.rst
+docs/version.rst
+docs/_static/.empty
+docs/development/getting-started.rst
+docs/development/index.rst
+docs/development/release-process.rst
+docs/development/reviewing-patches.rst
+docs/development/submitting-patches.rst
+packaging/__about__.py
+packaging/__init__.py
+packaging/_manylinux.py
+packaging/_musllinux.py
+packaging/_structures.py
+packaging/markers.py
+packaging/py.typed
+packaging/requirements.py
+packaging/specifiers.py
+packaging/tags.py
+packaging/utils.py
+packaging/version.py
+packaging.egg-info/PKG-INFO
+packaging.egg-info/SOURCES.txt
+packaging.egg-info/dependency_links.txt
+packaging.egg-info/requires.txt
+packaging.egg-info/top_level.txt
+tests/__init__.py
+tests/test_manylinux.py
+tests/test_markers.py
+tests/test_musllinux.py
+tests/test_requirements.py
+tests/test_specifiers.py
+tests/test_structures.py
+tests/test_tags.py
+tests/test_utils.py
+tests/test_version.py
+tests/manylinux/hello-world-armv7l-armel
+tests/manylinux/hello-world-armv7l-armhf
+tests/manylinux/hello-world-invalid-class
+tests/manylinux/hello-world-invalid-data
+tests/manylinux/hello-world-invalid-magic
+tests/manylinux/hello-world-s390x-s390x
+tests/manylinux/hello-world-too-short
+tests/manylinux/hello-world-x86_64-amd64
+tests/manylinux/hello-world-x86_64-i386
+tests/manylinux/hello-world-x86_64-x32
+tests/musllinux/glibc-x86_64
+tests/musllinux/musl-aarch64
+tests/musllinux/musl-i386
+tests/musllinux/musl-x86_64 \ No newline at end of file
diff --git a/packaging.egg-info/dependency_links.txt b/packaging.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/packaging.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/packaging.egg-info/requires.txt b/packaging.egg-info/requires.txt
new file mode 100644
index 0000000..f6e4a46
--- /dev/null
+++ b/packaging.egg-info/requires.txt
@@ -0,0 +1 @@
+pyparsing!=3.0.5,>=2.0.2
diff --git a/packaging.egg-info/top_level.txt b/packaging.egg-info/top_level.txt
new file mode 100644
index 0000000..748809f
--- /dev/null
+++ b/packaging.egg-info/top_level.txt
@@ -0,0 +1 @@
+packaging
diff --git a/packaging/__about__.py b/packaging/__about__.py
new file mode 100644
index 0000000..3551bc2
--- /dev/null
+++ b/packaging/__about__.py
@@ -0,0 +1,26 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+__all__ = [
+ "__title__",
+ "__summary__",
+ "__uri__",
+ "__version__",
+ "__author__",
+ "__email__",
+ "__license__",
+ "__copyright__",
+]
+
+__title__ = "packaging"
+__summary__ = "Core utilities for Python packages"
+__uri__ = "https://github.com/pypa/packaging"
+
+__version__ = "21.3"
+
+__author__ = "Donald Stufft and individual contributors"
+__email__ = "donald@stufft.io"
+
+__license__ = "BSD-2-Clause or Apache-2.0"
+__copyright__ = "2014-2019 %s" % __author__
diff --git a/packaging/__init__.py b/packaging/__init__.py
new file mode 100644
index 0000000..3c50c5d
--- /dev/null
+++ b/packaging/__init__.py
@@ -0,0 +1,25 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from .__about__ import (
+ __author__,
+ __copyright__,
+ __email__,
+ __license__,
+ __summary__,
+ __title__,
+ __uri__,
+ __version__,
+)
+
+__all__ = [
+ "__title__",
+ "__summary__",
+ "__uri__",
+ "__version__",
+ "__author__",
+ "__email__",
+ "__license__",
+ "__copyright__",
+]
diff --git a/packaging/_manylinux.py b/packaging/_manylinux.py
new file mode 100644
index 0000000..4c379aa
--- /dev/null
+++ b/packaging/_manylinux.py
@@ -0,0 +1,301 @@
+import collections
+import functools
+import os
+import re
+import struct
+import sys
+import warnings
+from typing import IO, Dict, Iterator, NamedTuple, Optional, Tuple
+
+
+# Python does not provide platform information at sufficient granularity to
+# identify the architecture of the running executable in some cases, so we
+# determine it dynamically by reading the information from the running
+# process. This only applies on Linux, which uses the ELF format.
+class _ELFFileHeader:
+ # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
+ class _InvalidELFFileHeader(ValueError):
+ """
+ An invalid ELF file header was found.
+ """
+
+ ELF_MAGIC_NUMBER = 0x7F454C46
+ ELFCLASS32 = 1
+ ELFCLASS64 = 2
+ ELFDATA2LSB = 1
+ ELFDATA2MSB = 2
+ EM_386 = 3
+ EM_S390 = 22
+ EM_ARM = 40
+ EM_X86_64 = 62
+ EF_ARM_ABIMASK = 0xFF000000
+ EF_ARM_ABI_VER5 = 0x05000000
+ EF_ARM_ABI_FLOAT_HARD = 0x00000400
+
+ def __init__(self, file: IO[bytes]) -> None:
+ def unpack(fmt: str) -> int:
+ try:
+ data = file.read(struct.calcsize(fmt))
+ result: Tuple[int, ...] = struct.unpack(fmt, data)
+ except struct.error:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ return result[0]
+
+ self.e_ident_magic = unpack(">I")
+ if self.e_ident_magic != self.ELF_MAGIC_NUMBER:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ self.e_ident_class = unpack("B")
+ if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ self.e_ident_data = unpack("B")
+ if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ self.e_ident_version = unpack("B")
+ self.e_ident_osabi = unpack("B")
+ self.e_ident_abiversion = unpack("B")
+ self.e_ident_pad = file.read(7)
+ format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H"
+ format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I"
+ format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q"
+ format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q
+ self.e_type = unpack(format_h)
+ self.e_machine = unpack(format_h)
+ self.e_version = unpack(format_i)
+ self.e_entry = unpack(format_p)
+ self.e_phoff = unpack(format_p)
+ self.e_shoff = unpack(format_p)
+ self.e_flags = unpack(format_i)
+ self.e_ehsize = unpack(format_h)
+ self.e_phentsize = unpack(format_h)
+ self.e_phnum = unpack(format_h)
+ self.e_shentsize = unpack(format_h)
+ self.e_shnum = unpack(format_h)
+ self.e_shstrndx = unpack(format_h)
+
+
+def _get_elf_header() -> Optional[_ELFFileHeader]:
+ try:
+ with open(sys.executable, "rb") as f:
+ elf_header = _ELFFileHeader(f)
+ except (OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader):
+ return None
+ return elf_header
+
+
+def _is_linux_armhf() -> bool:
+ # hard-float ABI can be detected from the ELF header of the running
+ # process
+ # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
+ elf_header = _get_elf_header()
+ if elf_header is None:
+ return False
+ result = elf_header.e_ident_class == elf_header.ELFCLASS32
+ result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+ result &= elf_header.e_machine == elf_header.EM_ARM
+ result &= (
+ elf_header.e_flags & elf_header.EF_ARM_ABIMASK
+ ) == elf_header.EF_ARM_ABI_VER5
+ result &= (
+ elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD
+ ) == elf_header.EF_ARM_ABI_FLOAT_HARD
+ return result
+
+
+def _is_linux_i686() -> bool:
+ elf_header = _get_elf_header()
+ if elf_header is None:
+ return False
+ result = elf_header.e_ident_class == elf_header.ELFCLASS32
+ result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+ result &= elf_header.e_machine == elf_header.EM_386
+ return result
+
+
+def _have_compatible_abi(arch: str) -> bool:
+ if arch == "armv7l":
+ return _is_linux_armhf()
+ if arch == "i686":
+ return _is_linux_i686()
+ return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"}
+
+
+# If glibc ever changes its major version, we need to know what the last
+# minor version was, so we can build the complete list of all versions.
+# For now, guess what the highest minor version might be, assume it will
+# be 50 for testing. Once this actually happens, update the dictionary
+# with the actual value.
+_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50)
+
+
+class _GLibCVersion(NamedTuple):
+ major: int
+ minor: int
+
+
+def _glibc_version_string_confstr() -> Optional[str]:
+ """
+ Primary implementation of glibc_version_string using os.confstr.
+ """
+ # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
+ # to be broken or missing. This strategy is used in the standard library
+ # platform module.
+ # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
+ try:
+ # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17".
+ version_string = os.confstr("CS_GNU_LIBC_VERSION")
+ assert version_string is not None
+ _, version = version_string.split()
+ except (AssertionError, AttributeError, OSError, ValueError):
+ # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
+ return None
+ return version
+
+
+def _glibc_version_string_ctypes() -> Optional[str]:
+ """
+ Fallback implementation of glibc_version_string using ctypes.
+ """
+ try:
+ import ctypes
+ except ImportError:
+ return None
+
+ # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
+ # manpage says, "If filename is NULL, then the returned handle is for the
+ # main program". This way we can let the linker do the work to figure out
+ # which libc our process is actually using.
+ #
+ # We must also handle the special case where the executable is not a
+ # dynamically linked executable. This can occur when using musl libc,
+ # for example. In this situation, dlopen() will error, leading to an
+ # OSError. Interestingly, at least in the case of musl, there is no
+ # errno set on the OSError. The single string argument used to construct
+ # OSError comes from libc itself and is therefore not portable to
+ # hard code here. In any case, failure to call dlopen() means we
+ # can proceed, so we bail on our attempt.
+ try:
+ process_namespace = ctypes.CDLL(None)
+ except OSError:
+ return None
+
+ try:
+ gnu_get_libc_version = process_namespace.gnu_get_libc_version
+ except AttributeError:
+ # Symbol doesn't exist -> therefore, we are not linked to
+ # glibc.
+ return None
+
+ # Call gnu_get_libc_version, which returns a string like "2.5"
+ gnu_get_libc_version.restype = ctypes.c_char_p
+ version_str: str = gnu_get_libc_version()
+ # py2 / py3 compatibility:
+ if not isinstance(version_str, str):
+ version_str = version_str.decode("ascii")
+
+ return version_str
+
+
+def _glibc_version_string() -> Optional[str]:
+ """Returns glibc version string, or None if not using glibc."""
+ return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
+
+
+def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
+ """Parse glibc version.
+
+ We use a regexp instead of str.split because we want to discard any
+ random junk that might come after the minor version -- this might happen
+ in patched/forked versions of glibc (e.g. Linaro's version of glibc
+ uses version strings like "2.20-2014.11"). See gh-3588.
+ """
+ m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
+ if not m:
+ warnings.warn(
+ "Expected glibc version with 2 components major.minor,"
+ " got: %s" % version_str,
+ RuntimeWarning,
+ )
+ return -1, -1
+ return int(m.group("major")), int(m.group("minor"))
+
+
+@functools.lru_cache()
+def _get_glibc_version() -> Tuple[int, int]:
+ version_str = _glibc_version_string()
+ if version_str is None:
+ return (-1, -1)
+ return _parse_glibc_version(version_str)
+
+
+# From PEP 513, PEP 600
+def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool:
+ sys_glibc = _get_glibc_version()
+ if sys_glibc < version:
+ return False
+ # Check for presence of _manylinux module.
+ try:
+ import _manylinux # noqa
+ except ImportError:
+ return True
+ if hasattr(_manylinux, "manylinux_compatible"):
+ result = _manylinux.manylinux_compatible(version[0], version[1], arch)
+ if result is not None:
+ return bool(result)
+ return True
+ if version == _GLibCVersion(2, 5):
+ if hasattr(_manylinux, "manylinux1_compatible"):
+ return bool(_manylinux.manylinux1_compatible)
+ if version == _GLibCVersion(2, 12):
+ if hasattr(_manylinux, "manylinux2010_compatible"):
+ return bool(_manylinux.manylinux2010_compatible)
+ if version == _GLibCVersion(2, 17):
+ if hasattr(_manylinux, "manylinux2014_compatible"):
+ return bool(_manylinux.manylinux2014_compatible)
+ return True
+
+
+_LEGACY_MANYLINUX_MAP = {
+ # CentOS 7 w/ glibc 2.17 (PEP 599)
+ (2, 17): "manylinux2014",
+ # CentOS 6 w/ glibc 2.12 (PEP 571)
+ (2, 12): "manylinux2010",
+ # CentOS 5 w/ glibc 2.5 (PEP 513)
+ (2, 5): "manylinux1",
+}
+
+
+def platform_tags(linux: str, arch: str) -> Iterator[str]:
+ if not _have_compatible_abi(arch):
+ return
+ # Oldest glibc to be supported regardless of architecture is (2, 17).
+ too_old_glibc2 = _GLibCVersion(2, 16)
+ if arch in {"x86_64", "i686"}:
+ # On x86/i686 also oldest glibc to be supported is (2, 5).
+ too_old_glibc2 = _GLibCVersion(2, 4)
+ current_glibc = _GLibCVersion(*_get_glibc_version())
+ glibc_max_list = [current_glibc]
+ # We can assume compatibility across glibc major versions.
+ # https://sourceware.org/bugzilla/show_bug.cgi?id=24636
+ #
+ # Build a list of maximum glibc versions so that we can
+ # output the canonical list of all glibc from current_glibc
+ # down to too_old_glibc2, including all intermediary versions.
+ for glibc_major in range(current_glibc.major - 1, 1, -1):
+ glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
+ glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
+ for glibc_max in glibc_max_list:
+ if glibc_max.major == too_old_glibc2.major:
+ min_minor = too_old_glibc2.minor
+ else:
+ # For other glibc major versions oldest supported is (x, 0).
+ min_minor = -1
+ for glibc_minor in range(glibc_max.minor, min_minor, -1):
+ glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
+ tag = "manylinux_{}_{}".format(*glibc_version)
+ if _is_compatible(tag, arch, glibc_version):
+ yield linux.replace("linux", tag)
+ # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
+ if glibc_version in _LEGACY_MANYLINUX_MAP:
+ legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
+ if _is_compatible(legacy_tag, arch, glibc_version):
+ yield linux.replace("linux", legacy_tag)
diff --git a/packaging/_musllinux.py b/packaging/_musllinux.py
new file mode 100644
index 0000000..8ac3059
--- /dev/null
+++ b/packaging/_musllinux.py
@@ -0,0 +1,136 @@
+"""PEP 656 support.
+
+This module implements logic to detect if the currently running Python is
+linked against musl, and what musl version is used.
+"""
+
+import contextlib
+import functools
+import operator
+import os
+import re
+import struct
+import subprocess
+import sys
+from typing import IO, Iterator, NamedTuple, Optional, Tuple
+
+
+def _read_unpacked(f: IO[bytes], fmt: str) -> Tuple[int, ...]:
+ return struct.unpack(fmt, f.read(struct.calcsize(fmt)))
+
+
+def _parse_ld_musl_from_elf(f: IO[bytes]) -> Optional[str]:
+ """Detect musl libc location by parsing the Python executable.
+
+ Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
+ ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
+ """
+ f.seek(0)
+ try:
+ ident = _read_unpacked(f, "16B")
+ except struct.error:
+ return None
+ if ident[:4] != tuple(b"\x7fELF"): # Invalid magic, not ELF.
+ return None
+ f.seek(struct.calcsize("HHI"), 1) # Skip file type, machine, and version.
+
+ try:
+ # e_fmt: Format for program header.
+ # p_fmt: Format for section header.
+ # p_idx: Indexes to find p_type, p_offset, and p_filesz.
+ e_fmt, p_fmt, p_idx = {
+ 1: ("IIIIHHH", "IIIIIIII", (0, 1, 4)), # 32-bit.
+ 2: ("QQQIHHH", "IIQQQQQQ", (0, 2, 5)), # 64-bit.
+ }[ident[4]]
+ except KeyError:
+ return None
+ else:
+ p_get = operator.itemgetter(*p_idx)
+
+ # Find the interpreter section and return its content.
+ try:
+ _, e_phoff, _, _, _, e_phentsize, e_phnum = _read_unpacked(f, e_fmt)
+ except struct.error:
+ return None
+ for i in range(e_phnum + 1):
+ f.seek(e_phoff + e_phentsize * i)
+ try:
+ p_type, p_offset, p_filesz = p_get(_read_unpacked(f, p_fmt))
+ except struct.error:
+ return None
+ if p_type != 3: # Not PT_INTERP.
+ continue
+ f.seek(p_offset)
+ interpreter = os.fsdecode(f.read(p_filesz)).strip("\0")
+ if "musl" not in interpreter:
+ return None
+ return interpreter
+ return None
+
+
+class _MuslVersion(NamedTuple):
+ major: int
+ minor: int
+
+
+def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
+ lines = [n for n in (n.strip() for n in output.splitlines()) if n]
+ if len(lines) < 2 or lines[0][:4] != "musl":
+ return None
+ m = re.match(r"Version (\d+)\.(\d+)", lines[1])
+ if not m:
+ return None
+ return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
+
+
+@functools.lru_cache()
+def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
+ """Detect currently-running musl runtime version.
+
+ This is done by checking the specified executable's dynamic linking
+ information, and invoking the loader to parse its output for a version
+ string. If the loader is musl, the output would be something like::
+
+ musl libc (x86_64)
+ Version 1.2.2
+ Dynamic Program Loader
+ """
+ with contextlib.ExitStack() as stack:
+ try:
+ f = stack.enter_context(open(executable, "rb"))
+ except OSError:
+ return None
+ ld = _parse_ld_musl_from_elf(f)
+ if not ld:
+ return None
+ proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True)
+ return _parse_musl_version(proc.stderr)
+
+
+def platform_tags(arch: str) -> Iterator[str]:
+ """Generate musllinux tags compatible to the current platform.
+
+ :param arch: Should be the part of platform tag after the ``linux_``
+ prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a
+ prerequisite for the current platform to be musllinux-compatible.
+
+ :returns: An iterator of compatible musllinux tags.
+ """
+ sys_musl = _get_musl_version(sys.executable)
+ if sys_musl is None: # Python not dynamically linked against musl.
+ return
+ for minor in range(sys_musl.minor, -1, -1):
+ yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
+
+
+if __name__ == "__main__": # pragma: no cover
+ import sysconfig
+
+ plat = sysconfig.get_platform()
+ assert plat.startswith("linux-"), "not linux"
+
+ print("plat:", plat)
+ print("musl:", _get_musl_version(sys.executable))
+ print("tags:", end=" ")
+ for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])):
+ print(t, end="\n ")
diff --git a/packaging/_structures.py b/packaging/_structures.py
new file mode 100644
index 0000000..90a6465
--- /dev/null
+++ b/packaging/_structures.py
@@ -0,0 +1,61 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+
+class InfinityType:
+ def __repr__(self) -> str:
+ return "Infinity"
+
+ def __hash__(self) -> int:
+ return hash(repr(self))
+
+ def __lt__(self, other: object) -> bool:
+ return False
+
+ def __le__(self, other: object) -> bool:
+ return False
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, self.__class__)
+
+ def __gt__(self, other: object) -> bool:
+ return True
+
+ def __ge__(self, other: object) -> bool:
+ return True
+
+ def __neg__(self: object) -> "NegativeInfinityType":
+ return NegativeInfinity
+
+
+Infinity = InfinityType()
+
+
+class NegativeInfinityType:
+ def __repr__(self) -> str:
+ return "-Infinity"
+
+ def __hash__(self) -> int:
+ return hash(repr(self))
+
+ def __lt__(self, other: object) -> bool:
+ return True
+
+ def __le__(self, other: object) -> bool:
+ return True
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, self.__class__)
+
+ def __gt__(self, other: object) -> bool:
+ return False
+
+ def __ge__(self, other: object) -> bool:
+ return False
+
+ def __neg__(self: object) -> InfinityType:
+ return Infinity
+
+
+NegativeInfinity = NegativeInfinityType()
diff --git a/packaging/markers.py b/packaging/markers.py
new file mode 100644
index 0000000..cb640e8
--- /dev/null
+++ b/packaging/markers.py
@@ -0,0 +1,304 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import operator
+import os
+import platform
+import sys
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+from pyparsing import ( # noqa: N817
+ Forward,
+ Group,
+ Literal as L,
+ ParseException,
+ ParseResults,
+ QuotedString,
+ ZeroOrMore,
+ stringEnd,
+ stringStart,
+)
+
+from .specifiers import InvalidSpecifier, Specifier
+
+__all__ = [
+ "InvalidMarker",
+ "UndefinedComparison",
+ "UndefinedEnvironmentName",
+ "Marker",
+ "default_environment",
+]
+
+Operator = Callable[[str, str], bool]
+
+
+class InvalidMarker(ValueError):
+ """
+ An invalid marker was found, users should refer to PEP 508.
+ """
+
+
+class UndefinedComparison(ValueError):
+ """
+ An invalid operation was attempted on a value that doesn't support it.
+ """
+
+
+class UndefinedEnvironmentName(ValueError):
+ """
+ A name was attempted to be used that does not exist inside of the
+ environment.
+ """
+
+
+class Node:
+ def __init__(self, value: Any) -> None:
+ self.value = value
+
+ def __str__(self) -> str:
+ return str(self.value)
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__}('{self}')>"
+
+ def serialize(self) -> str:
+ raise NotImplementedError
+
+
+class Variable(Node):
+ def serialize(self) -> str:
+ return str(self)
+
+
+class Value(Node):
+ def serialize(self) -> str:
+ return f'"{self}"'
+
+
+class Op(Node):
+ def serialize(self) -> str:
+ return str(self)
+
+
+VARIABLE = (
+ L("implementation_version")
+ | L("platform_python_implementation")
+ | L("implementation_name")
+ | L("python_full_version")
+ | L("platform_release")
+ | L("platform_version")
+ | L("platform_machine")
+ | L("platform_system")
+ | L("python_version")
+ | L("sys_platform")
+ | L("os_name")
+ | L("os.name") # PEP-345
+ | L("sys.platform") # PEP-345
+ | L("platform.version") # PEP-345
+ | L("platform.machine") # PEP-345
+ | L("platform.python_implementation") # PEP-345
+ | L("python_implementation") # undocumented setuptools legacy
+ | L("extra") # PEP-508
+)
+ALIASES = {
+ "os.name": "os_name",
+ "sys.platform": "sys_platform",
+ "platform.version": "platform_version",
+ "platform.machine": "platform_machine",
+ "platform.python_implementation": "platform_python_implementation",
+ "python_implementation": "platform_python_implementation",
+}
+VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))
+
+VERSION_CMP = (
+ L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<")
+)
+
+MARKER_OP = VERSION_CMP | L("not in") | L("in")
+MARKER_OP.setParseAction(lambda s, l, t: Op(t[0]))
+
+MARKER_VALUE = QuotedString("'") | QuotedString('"')
+MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))
+
+BOOLOP = L("and") | L("or")
+
+MARKER_VAR = VARIABLE | MARKER_VALUE
+
+MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)
+MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))
+
+LPAREN = L("(").suppress()
+RPAREN = L(")").suppress()
+
+MARKER_EXPR = Forward()
+MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)
+MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)
+
+MARKER = stringStart + MARKER_EXPR + stringEnd
+
+
+def _coerce_parse_result(results: Union[ParseResults, List[Any]]) -> List[Any]:
+ if isinstance(results, ParseResults):
+ return [_coerce_parse_result(i) for i in results]
+ else:
+ return results
+
+
+def _format_marker(
+ marker: Union[List[str], Tuple[Node, ...], str], first: Optional[bool] = True
+) -> str:
+
+ assert isinstance(marker, (list, tuple, str))
+
+ # Sometimes we have a structure like [[...]] which is a single item list
+ # where the single item is itself it's own list. In that case we want skip
+ # the rest of this function so that we don't get extraneous () on the
+ # outside.
+ if (
+ isinstance(marker, list)
+ and len(marker) == 1
+ and isinstance(marker[0], (list, tuple))
+ ):
+ return _format_marker(marker[0])
+
+ if isinstance(marker, list):
+ inner = (_format_marker(m, first=False) for m in marker)
+ if first:
+ return " ".join(inner)
+ else:
+ return "(" + " ".join(inner) + ")"
+ elif isinstance(marker, tuple):
+ return " ".join([m.serialize() for m in marker])
+ else:
+ return marker
+
+
+_operators: Dict[str, Operator] = {
+ "in": lambda lhs, rhs: lhs in rhs,
+ "not in": lambda lhs, rhs: lhs not in rhs,
+ "<": operator.lt,
+ "<=": operator.le,
+ "==": operator.eq,
+ "!=": operator.ne,
+ ">=": operator.ge,
+ ">": operator.gt,
+}
+
+
+def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
+ try:
+ spec = Specifier("".join([op.serialize(), rhs]))
+ except InvalidSpecifier:
+ pass
+ else:
+ return spec.contains(lhs)
+
+ oper: Optional[Operator] = _operators.get(op.serialize())
+ if oper is None:
+ raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
+
+ return oper(lhs, rhs)
+
+
+class Undefined:
+ pass
+
+
+_undefined = Undefined()
+
+
+def _get_env(environment: Dict[str, str], name: str) -> str:
+ value: Union[str, Undefined] = environment.get(name, _undefined)
+
+ if isinstance(value, Undefined):
+ raise UndefinedEnvironmentName(
+ f"{name!r} does not exist in evaluation environment."
+ )
+
+ return value
+
+
+def _evaluate_markers(markers: List[Any], environment: Dict[str, str]) -> bool:
+ groups: List[List[bool]] = [[]]
+
+ for marker in markers:
+ assert isinstance(marker, (list, tuple, str))
+
+ if isinstance(marker, list):
+ groups[-1].append(_evaluate_markers(marker, environment))
+ elif isinstance(marker, tuple):
+ lhs, op, rhs = marker
+
+ if isinstance(lhs, Variable):
+ lhs_value = _get_env(environment, lhs.value)
+ rhs_value = rhs.value
+ else:
+ lhs_value = lhs.value
+ rhs_value = _get_env(environment, rhs.value)
+
+ groups[-1].append(_eval_op(lhs_value, op, rhs_value))
+ else:
+ assert marker in ["and", "or"]
+ if marker == "or":
+ groups.append([])
+
+ return any(all(item) for item in groups)
+
+
+def format_full_version(info: "sys._version_info") -> str:
+ version = "{0.major}.{0.minor}.{0.micro}".format(info)
+ kind = info.releaselevel
+ if kind != "final":
+ version += kind[0] + str(info.serial)
+ return version
+
+
+def default_environment() -> Dict[str, str]:
+ iver = format_full_version(sys.implementation.version)
+ implementation_name = sys.implementation.name
+ return {
+ "implementation_name": implementation_name,
+ "implementation_version": iver,
+ "os_name": os.name,
+ "platform_machine": platform.machine(),
+ "platform_release": platform.release(),
+ "platform_system": platform.system(),
+ "platform_version": platform.version(),
+ "python_full_version": platform.python_version(),
+ "platform_python_implementation": platform.python_implementation(),
+ "python_version": ".".join(platform.python_version_tuple()[:2]),
+ "sys_platform": sys.platform,
+ }
+
+
+class Marker:
+ def __init__(self, marker: str) -> None:
+ try:
+ self._markers = _coerce_parse_result(MARKER.parseString(marker))
+ except ParseException as e:
+ raise InvalidMarker(
+ f"Invalid marker: {marker!r}, parse error at "
+ f"{marker[e.loc : e.loc + 8]!r}"
+ )
+
+ def __str__(self) -> str:
+ return _format_marker(self._markers)
+
+ def __repr__(self) -> str:
+ return f"<Marker('{self}')>"
+
+ def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool:
+ """Evaluate a marker.
+
+ Return the boolean from evaluating the given marker against the
+ environment. environment is an optional argument to override all or
+ part of the determined environment.
+
+ The environment is determined from the current Python process.
+ """
+ current_environment = default_environment()
+ if environment is not None:
+ current_environment.update(environment)
+
+ return _evaluate_markers(self._markers, current_environment)
diff --git a/packaging/py.typed b/packaging/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packaging/py.typed
diff --git a/packaging/requirements.py b/packaging/requirements.py
new file mode 100644
index 0000000..53f9a3a
--- /dev/null
+++ b/packaging/requirements.py
@@ -0,0 +1,146 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import re
+import string
+import urllib.parse
+from typing import List, Optional as TOptional, Set
+
+from pyparsing import ( # noqa
+ Combine,
+ Literal as L,
+ Optional,
+ ParseException,
+ Regex,
+ Word,
+ ZeroOrMore,
+ originalTextFor,
+ stringEnd,
+ stringStart,
+)
+
+from .markers import MARKER_EXPR, Marker
+from .specifiers import LegacySpecifier, Specifier, SpecifierSet
+
+
+class InvalidRequirement(ValueError):
+ """
+ An invalid requirement was found, users should refer to PEP 508.
+ """
+
+
+ALPHANUM = Word(string.ascii_letters + string.digits)
+
+LBRACKET = L("[").suppress()
+RBRACKET = L("]").suppress()
+LPAREN = L("(").suppress()
+RPAREN = L(")").suppress()
+COMMA = L(",").suppress()
+SEMICOLON = L(";").suppress()
+AT = L("@").suppress()
+
+PUNCTUATION = Word("-_.")
+IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)
+IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))
+
+NAME = IDENTIFIER("name")
+EXTRA = IDENTIFIER
+
+URI = Regex(r"[^ ]+")("url")
+URL = AT + URI
+
+EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)
+EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras")
+
+VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)
+VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)
+
+VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY
+VERSION_MANY = Combine(
+ VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False
+)("_raw_spec")
+_VERSION_SPEC = Optional((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)
+_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "")
+
+VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier")
+VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
+
+MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
+MARKER_EXPR.setParseAction(
+ lambda s, l, t: Marker(s[t._original_start : t._original_end])
+)
+MARKER_SEPARATOR = SEMICOLON
+MARKER = MARKER_SEPARATOR + MARKER_EXPR
+
+VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)
+URL_AND_MARKER = URL + Optional(MARKER)
+
+NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
+
+REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd
+# pyparsing isn't thread safe during initialization, so we do it eagerly, see
+# issue #104
+REQUIREMENT.parseString("x[]")
+
+
+class Requirement:
+ """Parse a requirement.
+
+ Parse a given requirement string into its parts, such as name, specifier,
+ URL, and extras. Raises InvalidRequirement on a badly-formed requirement
+ string.
+ """
+
+ # TODO: Can we test whether something is contained within a requirement?
+ # If so how do we do that? Do we need to test against the _name_ of
+ # the thing as well as the version? What about the markers?
+ # TODO: Can we normalize the name and extra name?
+
+ def __init__(self, requirement_string: str) -> None:
+ try:
+ req = REQUIREMENT.parseString(requirement_string)
+ except ParseException as e:
+ raise InvalidRequirement(
+ f'Parse error at "{ requirement_string[e.loc : e.loc + 8]!r}": {e.msg}'
+ )
+
+ self.name: str = req.name
+ if req.url:
+ parsed_url = urllib.parse.urlparse(req.url)
+ if parsed_url.scheme == "file":
+ if urllib.parse.urlunparse(parsed_url) != req.url:
+ raise InvalidRequirement("Invalid URL given")
+ elif not (parsed_url.scheme and parsed_url.netloc) or (
+ not parsed_url.scheme and not parsed_url.netloc
+ ):
+ raise InvalidRequirement(f"Invalid URL: {req.url}")
+ self.url: TOptional[str] = req.url
+ else:
+ self.url = None
+ self.extras: Set[str] = set(req.extras.asList() if req.extras else [])
+ self.specifier: SpecifierSet = SpecifierSet(req.specifier)
+ self.marker: TOptional[Marker] = req.marker if req.marker else None
+
+ def __str__(self) -> str:
+ parts: List[str] = [self.name]
+
+ if self.extras:
+ formatted_extras = ",".join(sorted(self.extras))
+ parts.append(f"[{formatted_extras}]")
+
+ if self.specifier:
+ parts.append(str(self.specifier))
+
+ if self.url:
+ parts.append(f"@ {self.url}")
+ if self.marker:
+ parts.append(" ")
+
+ if self.marker:
+ parts.append(f"; {self.marker}")
+
+ return "".join(parts)
+
+ def __repr__(self) -> str:
+ return f"<Requirement('{self}')>"
diff --git a/packaging/specifiers.py b/packaging/specifiers.py
new file mode 100644
index 0000000..0e218a6
--- /dev/null
+++ b/packaging/specifiers.py
@@ -0,0 +1,802 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import abc
+import functools
+import itertools
+import re
+import warnings
+from typing import (
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Pattern,
+ Set,
+ Tuple,
+ TypeVar,
+ Union,
+)
+
+from .utils import canonicalize_version
+from .version import LegacyVersion, Version, parse
+
+ParsedVersion = Union[Version, LegacyVersion]
+UnparsedVersion = Union[Version, LegacyVersion, str]
+VersionTypeVar = TypeVar("VersionTypeVar", bound=UnparsedVersion)
+CallableOperator = Callable[[ParsedVersion, str], bool]
+
+
+class InvalidSpecifier(ValueError):
+ """
+ An invalid specifier was found, users should refer to PEP 440.
+ """
+
+
+class BaseSpecifier(metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def __str__(self) -> str:
+ """
+ Returns the str representation of this Specifier like object. This
+ should be representative of the Specifier itself.
+ """
+
+ @abc.abstractmethod
+ def __hash__(self) -> int:
+ """
+ Returns a hash value for this Specifier like object.
+ """
+
+ @abc.abstractmethod
+ def __eq__(self, other: object) -> bool:
+ """
+ Returns a boolean representing whether or not the two Specifier like
+ objects are equal.
+ """
+
+ @abc.abstractproperty
+ def prereleases(self) -> Optional[bool]:
+ """
+ Returns whether or not pre-releases as a whole are allowed by this
+ specifier.
+ """
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ """
+ Sets whether or not pre-releases as a whole are allowed by this
+ specifier.
+ """
+
+ @abc.abstractmethod
+ def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
+ """
+ Determines if the given item is contained within this specifier.
+ """
+
+ @abc.abstractmethod
+ def filter(
+ self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
+ ) -> Iterable[VersionTypeVar]:
+ """
+ Takes an iterable of items and filters them so that only items which
+ are contained within this specifier are allowed in it.
+ """
+
+
+class _IndividualSpecifier(BaseSpecifier):
+
+ _operators: Dict[str, str] = {}
+ _regex: Pattern[str]
+
+ def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
+ match = self._regex.search(spec)
+ if not match:
+ raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
+
+ self._spec: Tuple[str, str] = (
+ match.group("operator").strip(),
+ match.group("version").strip(),
+ )
+
+ # Store whether or not this Specifier should accept prereleases
+ self._prereleases = prereleases
+
+ def __repr__(self) -> str:
+ pre = (
+ f", prereleases={self.prereleases!r}"
+ if self._prereleases is not None
+ else ""
+ )
+
+ return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
+
+ def __str__(self) -> str:
+ return "{}{}".format(*self._spec)
+
+ @property
+ def _canonical_spec(self) -> Tuple[str, str]:
+ return self._spec[0], canonicalize_version(self._spec[1])
+
+ def __hash__(self) -> int:
+ return hash(self._canonical_spec)
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, str):
+ try:
+ other = self.__class__(str(other))
+ except InvalidSpecifier:
+ return NotImplemented
+ elif not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self._canonical_spec == other._canonical_spec
+
+ def _get_operator(self, op: str) -> CallableOperator:
+ operator_callable: CallableOperator = getattr(
+ self, f"_compare_{self._operators[op]}"
+ )
+ return operator_callable
+
+ def _coerce_version(self, version: UnparsedVersion) -> ParsedVersion:
+ if not isinstance(version, (LegacyVersion, Version)):
+ version = parse(version)
+ return version
+
+ @property
+ def operator(self) -> str:
+ return self._spec[0]
+
+ @property
+ def version(self) -> str:
+ return self._spec[1]
+
+ @property
+ def prereleases(self) -> Optional[bool]:
+ return self._prereleases
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ self._prereleases = value
+
+ def __contains__(self, item: str) -> bool:
+ return self.contains(item)
+
+ def contains(
+ self, item: UnparsedVersion, prereleases: Optional[bool] = None
+ ) -> bool:
+
+ # Determine if prereleases are to be allowed or not.
+ if prereleases is None:
+ prereleases = self.prereleases
+
+ # Normalize item to a Version or LegacyVersion, this allows us to have
+ # a shortcut for ``"2.0" in Specifier(">=2")
+ normalized_item = self._coerce_version(item)
+
+ # Determine if we should be supporting prereleases in this specifier
+ # or not, if we do not support prereleases than we can short circuit
+ # logic if this version is a prereleases.
+ if normalized_item.is_prerelease and not prereleases:
+ return False
+
+ # Actually do the comparison to determine if this item is contained
+ # within this Specifier or not.
+ operator_callable: CallableOperator = self._get_operator(self.operator)
+ return operator_callable(normalized_item, self.version)
+
+ def filter(
+ self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
+ ) -> Iterable[VersionTypeVar]:
+
+ yielded = False
+ found_prereleases = []
+
+ kw = {"prereleases": prereleases if prereleases is not None else True}
+
+ # Attempt to iterate over all the values in the iterable and if any of
+ # them match, yield them.
+ for version in iterable:
+ parsed_version = self._coerce_version(version)
+
+ if self.contains(parsed_version, **kw):
+ # If our version is a prerelease, and we were not set to allow
+ # prereleases, then we'll store it for later in case nothing
+ # else matches this specifier.
+ if parsed_version.is_prerelease and not (
+ prereleases or self.prereleases
+ ):
+ found_prereleases.append(version)
+ # Either this is not a prerelease, or we should have been
+ # accepting prereleases from the beginning.
+ else:
+ yielded = True
+ yield version
+
+ # Now that we've iterated over everything, determine if we've yielded
+ # any values, and if we have not and we have any prereleases stored up
+ # then we will go ahead and yield the prereleases.
+ if not yielded and found_prereleases:
+ for version in found_prereleases:
+ yield version
+
+
+class LegacySpecifier(_IndividualSpecifier):
+
+ _regex_str = r"""
+ (?P<operator>(==|!=|<=|>=|<|>))
+ \s*
+ (?P<version>
+ [^,;\s)]* # Since this is a "legacy" specifier, and the version
+ # string can be just about anything, we match everything
+ # except for whitespace, a semi-colon for marker support,
+ # a closing paren since versions can be enclosed in
+ # them, and a comma since it's a version separator.
+ )
+ """
+
+ _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
+
+ _operators = {
+ "==": "equal",
+ "!=": "not_equal",
+ "<=": "less_than_equal",
+ ">=": "greater_than_equal",
+ "<": "less_than",
+ ">": "greater_than",
+ }
+
+ def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
+ super().__init__(spec, prereleases)
+
+ warnings.warn(
+ "Creating a LegacyVersion has been deprecated and will be "
+ "removed in the next major release",
+ DeprecationWarning,
+ )
+
+ def _coerce_version(self, version: UnparsedVersion) -> LegacyVersion:
+ if not isinstance(version, LegacyVersion):
+ version = LegacyVersion(str(version))
+ return version
+
+ def _compare_equal(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective == self._coerce_version(spec)
+
+ def _compare_not_equal(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective != self._coerce_version(spec)
+
+ def _compare_less_than_equal(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective <= self._coerce_version(spec)
+
+ def _compare_greater_than_equal(
+ self, prospective: LegacyVersion, spec: str
+ ) -> bool:
+ return prospective >= self._coerce_version(spec)
+
+ def _compare_less_than(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective < self._coerce_version(spec)
+
+ def _compare_greater_than(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective > self._coerce_version(spec)
+
+
+def _require_version_compare(
+ fn: Callable[["Specifier", ParsedVersion, str], bool]
+) -> Callable[["Specifier", ParsedVersion, str], bool]:
+ @functools.wraps(fn)
+ def wrapped(self: "Specifier", prospective: ParsedVersion, spec: str) -> bool:
+ if not isinstance(prospective, Version):
+ return False
+ return fn(self, prospective, spec)
+
+ return wrapped
+
+
+class Specifier(_IndividualSpecifier):
+
+ _regex_str = r"""
+ (?P<operator>(~=|==|!=|<=|>=|<|>|===))
+ (?P<version>
+ (?:
+ # The identity operators allow for an escape hatch that will
+ # do an exact string match of the version you wish to install.
+ # This will not be parsed by PEP 440 and we cannot determine
+ # any semantic meaning from it. This operator is discouraged
+ # but included entirely as an escape hatch.
+ (?<====) # Only match for the identity operator
+ \s*
+ [^\s]* # We just match everything, except for whitespace
+ # since we are only testing for strict identity.
+ )
+ |
+ (?:
+ # The (non)equality operators allow for wild card and local
+ # versions to be specified so we have to define these two
+ # operators separately to enable that.
+ (?<===|!=) # Only match for equals and not equals
+
+ \s*
+ v?
+ (?:[0-9]+!)? # epoch
+ [0-9]+(?:\.[0-9]+)* # release
+ (?: # pre release
+ [-_\.]?
+ (a|b|c|rc|alpha|beta|pre|preview)
+ [-_\.]?
+ [0-9]*
+ )?
+ (?: # post release
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+ )?
+
+ # You cannot use a wild card and a dev or local version
+ # together so group them with a | and make them optional.
+ (?:
+ (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
+ (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
+ |
+ \.\* # Wild card syntax of .*
+ )?
+ )
+ |
+ (?:
+ # The compatible operator requires at least two digits in the
+ # release segment.
+ (?<=~=) # Only match for the compatible operator
+
+ \s*
+ v?
+ (?:[0-9]+!)? # epoch
+ [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *)
+ (?: # pre release
+ [-_\.]?
+ (a|b|c|rc|alpha|beta|pre|preview)
+ [-_\.]?
+ [0-9]*
+ )?
+ (?: # post release
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+ )?
+ (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
+ )
+ |
+ (?:
+ # All other operators only allow a sub set of what the
+ # (non)equality operators do. Specifically they do not allow
+ # local versions to be specified nor do they allow the prefix
+ # matching wild cards.
+ (?<!==|!=|~=) # We have special cases for these
+ # operators so we want to make sure they
+ # don't match here.
+
+ \s*
+ v?
+ (?:[0-9]+!)? # epoch
+ [0-9]+(?:\.[0-9]+)* # release
+ (?: # pre release
+ [-_\.]?
+ (a|b|c|rc|alpha|beta|pre|preview)
+ [-_\.]?
+ [0-9]*
+ )?
+ (?: # post release
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+ )?
+ (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
+ )
+ )
+ """
+
+ _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
+
+ _operators = {
+ "~=": "compatible",
+ "==": "equal",
+ "!=": "not_equal",
+ "<=": "less_than_equal",
+ ">=": "greater_than_equal",
+ "<": "less_than",
+ ">": "greater_than",
+ "===": "arbitrary",
+ }
+
+ @_require_version_compare
+ def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool:
+
+ # Compatible releases have an equivalent combination of >= and ==. That
+ # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
+ # implement this in terms of the other specifiers instead of
+ # implementing it ourselves. The only thing we need to do is construct
+ # the other specifiers.
+
+ # We want everything but the last item in the version, but we want to
+ # ignore suffix segments.
+ prefix = ".".join(
+ list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
+ )
+
+ # Add the prefix notation to the end of our string
+ prefix += ".*"
+
+ return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(
+ prospective, prefix
+ )
+
+ @_require_version_compare
+ def _compare_equal(self, prospective: ParsedVersion, spec: str) -> bool:
+
+ # We need special logic to handle prefix matching
+ if spec.endswith(".*"):
+ # In the case of prefix matching we want to ignore local segment.
+ prospective = Version(prospective.public)
+ # Split the spec out by dots, and pretend that there is an implicit
+ # dot in between a release segment and a pre-release segment.
+ split_spec = _version_split(spec[:-2]) # Remove the trailing .*
+
+ # Split the prospective version out by dots, and pretend that there
+ # is an implicit dot in between a release segment and a pre-release
+ # segment.
+ split_prospective = _version_split(str(prospective))
+
+ # Shorten the prospective version to be the same length as the spec
+ # so that we can determine if the specifier is a prefix of the
+ # prospective version or not.
+ shortened_prospective = split_prospective[: len(split_spec)]
+
+ # Pad out our two sides with zeros so that they both equal the same
+ # length.
+ padded_spec, padded_prospective = _pad_version(
+ split_spec, shortened_prospective
+ )
+
+ return padded_prospective == padded_spec
+ else:
+ # Convert our spec string into a Version
+ spec_version = Version(spec)
+
+ # If the specifier does not have a local segment, then we want to
+ # act as if the prospective version also does not have a local
+ # segment.
+ if not spec_version.local:
+ prospective = Version(prospective.public)
+
+ return prospective == spec_version
+
+ @_require_version_compare
+ def _compare_not_equal(self, prospective: ParsedVersion, spec: str) -> bool:
+ return not self._compare_equal(prospective, spec)
+
+ @_require_version_compare
+ def _compare_less_than_equal(self, prospective: ParsedVersion, spec: str) -> bool:
+
+ # NB: Local version identifiers are NOT permitted in the version
+ # specifier, so local version labels can be universally removed from
+ # the prospective version.
+ return Version(prospective.public) <= Version(spec)
+
+ @_require_version_compare
+ def _compare_greater_than_equal(
+ self, prospective: ParsedVersion, spec: str
+ ) -> bool:
+
+ # NB: Local version identifiers are NOT permitted in the version
+ # specifier, so local version labels can be universally removed from
+ # the prospective version.
+ return Version(prospective.public) >= Version(spec)
+
+ @_require_version_compare
+ def _compare_less_than(self, prospective: ParsedVersion, spec_str: str) -> bool:
+
+ # Convert our spec to a Version instance, since we'll want to work with
+ # it as a version.
+ spec = Version(spec_str)
+
+ # Check to see if the prospective version is less than the spec
+ # version. If it's not we can short circuit and just return False now
+ # instead of doing extra unneeded work.
+ if not prospective < spec:
+ return False
+
+ # 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 not spec.is_prerelease and prospective.is_prerelease:
+ if Version(prospective.base_version) == Version(spec.base_version):
+ return False
+
+ # If we've gotten to here, it means that prospective version is both
+ # less than the spec version *and* it's not a pre-release of the same
+ # version in the spec.
+ return True
+
+ @_require_version_compare
+ def _compare_greater_than(self, prospective: ParsedVersion, spec_str: str) -> bool:
+
+ # Convert our spec to a Version instance, since we'll want to work with
+ # it as a version.
+ spec = Version(spec_str)
+
+ # Check to see if the prospective version is greater than the spec
+ # version. If it's not we can short circuit and just return False now
+ # instead of doing extra unneeded work.
+ if not prospective > spec:
+ return False
+
+ # 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 not spec.is_postrelease and prospective.is_postrelease:
+ if Version(prospective.base_version) == Version(spec.base_version):
+ return False
+
+ # Ensure that we do not allow a local version of the version mentioned
+ # in the specifier, which is technically greater than, to match.
+ if prospective.local is not None:
+ if Version(prospective.base_version) == Version(spec.base_version):
+ return False
+
+ # If we've gotten to here, it means that prospective version is both
+ # greater than the spec version *and* it's not a pre-release of the
+ # same version in the spec.
+ return True
+
+ def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
+ return str(prospective).lower() == str(spec).lower()
+
+ @property
+ def prereleases(self) -> bool:
+
+ # If there is an explicit prereleases set for this, then we'll just
+ # blindly use that.
+ if self._prereleases is not None:
+ return self._prereleases
+
+ # Look at all of our specifiers and determine if they are inclusive
+ # operators, and if they are if they are including an explicit
+ # prerelease.
+ operator, version = self._spec
+ if operator in ["==", ">=", "<=", "~=", "==="]:
+ # The == specifier can include a trailing .*, if it does we
+ # want to remove before parsing.
+ if operator == "==" and version.endswith(".*"):
+ version = version[:-2]
+
+ # Parse the version, and if it is a pre-release than this
+ # specifier allows pre-releases.
+ if parse(version).is_prerelease:
+ return True
+
+ return False
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ self._prereleases = value
+
+
+_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
+
+
+def _version_split(version: str) -> List[str]:
+ result: List[str] = []
+ for item in version.split("."):
+ match = _prefix_regex.search(item)
+ if match:
+ result.extend(match.groups())
+ else:
+ result.append(item)
+ return result
+
+
+def _is_not_suffix(segment: str) -> bool:
+ return not any(
+ segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
+ )
+
+
+def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]:
+ left_split, right_split = [], []
+
+ # Get the release segment of our versions
+ left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
+ right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
+
+ # Get the rest of our versions
+ left_split.append(left[len(left_split[0]) :])
+ right_split.append(right[len(right_split[0]) :])
+
+ # Insert our padding
+ left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
+ right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
+
+ return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))
+
+
+class SpecifierSet(BaseSpecifier):
+ def __init__(
+ self, specifiers: str = "", prereleases: Optional[bool] = None
+ ) -> None:
+
+ # Split on , to break each individual specifier into it's own item, and
+ # strip each item to remove leading/trailing whitespace.
+ split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
+
+ # Parsed each individual specifier, attempting first to make it a
+ # Specifier and falling back to a LegacySpecifier.
+ parsed: Set[_IndividualSpecifier] = set()
+ for specifier in split_specifiers:
+ try:
+ parsed.add(Specifier(specifier))
+ except InvalidSpecifier:
+ parsed.add(LegacySpecifier(specifier))
+
+ # Turn our parsed specifiers into a frozen set and save them for later.
+ self._specs = frozenset(parsed)
+
+ # Store our prereleases value so we can use it later to determine if
+ # we accept prereleases or not.
+ self._prereleases = prereleases
+
+ def __repr__(self) -> str:
+ pre = (
+ f", prereleases={self.prereleases!r}"
+ if self._prereleases is not None
+ else ""
+ )
+
+ return f"<SpecifierSet({str(self)!r}{pre})>"
+
+ def __str__(self) -> str:
+ return ",".join(sorted(str(s) for s in self._specs))
+
+ def __hash__(self) -> int:
+ return hash(self._specs)
+
+ def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet":
+ if isinstance(other, str):
+ other = SpecifierSet(other)
+ elif not isinstance(other, SpecifierSet):
+ return NotImplemented
+
+ specifier = SpecifierSet()
+ specifier._specs = frozenset(self._specs | other._specs)
+
+ if self._prereleases is None and other._prereleases is not None:
+ specifier._prereleases = other._prereleases
+ elif self._prereleases is not None and other._prereleases is None:
+ specifier._prereleases = self._prereleases
+ elif self._prereleases == other._prereleases:
+ specifier._prereleases = self._prereleases
+ else:
+ raise ValueError(
+ "Cannot combine SpecifierSets with True and False prerelease "
+ "overrides."
+ )
+
+ return specifier
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, (str, _IndividualSpecifier)):
+ other = SpecifierSet(str(other))
+ elif not isinstance(other, SpecifierSet):
+ return NotImplemented
+
+ return self._specs == other._specs
+
+ def __len__(self) -> int:
+ return len(self._specs)
+
+ def __iter__(self) -> Iterator[_IndividualSpecifier]:
+ return iter(self._specs)
+
+ @property
+ def prereleases(self) -> Optional[bool]:
+
+ # If we have been given an explicit prerelease modifier, then we'll
+ # pass that through here.
+ if self._prereleases is not None:
+ return self._prereleases
+
+ # If we don't have any specifiers, and we don't have a forced value,
+ # then we'll just return None since we don't know if this should have
+ # pre-releases or not.
+ if not self._specs:
+ return None
+
+ # Otherwise we'll see if any of the given specifiers accept
+ # prereleases, if any of them do we'll return True, otherwise False.
+ return any(s.prereleases for s in self._specs)
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ self._prereleases = value
+
+ def __contains__(self, item: UnparsedVersion) -> bool:
+ return self.contains(item)
+
+ def contains(
+ self, item: UnparsedVersion, prereleases: Optional[bool] = None
+ ) -> bool:
+
+ # Ensure that our item is a Version or LegacyVersion instance.
+ if not isinstance(item, (LegacyVersion, Version)):
+ item = parse(item)
+
+ # Determine if we're forcing a prerelease or not, if we're not forcing
+ # one for this particular filter call, then we'll use whatever the
+ # SpecifierSet thinks for whether or not we should support prereleases.
+ if prereleases is None:
+ prereleases = self.prereleases
+
+ # We can determine if we're going to allow pre-releases by looking to
+ # see if any of the underlying items supports them. If none of them do
+ # and this item is a pre-release then we do not allow it and we can
+ # short circuit that here.
+ # Note: This means that 1.0.dev1 would not be contained in something
+ # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0
+ if not prereleases and item.is_prerelease:
+ return False
+
+ # We simply dispatch to the underlying specs here to make sure that the
+ # given version is contained within all of them.
+ # Note: This use of all() here means that an empty set of specifiers
+ # will always return True, this is an explicit design decision.
+ return all(s.contains(item, prereleases=prereleases) for s in self._specs)
+
+ def filter(
+ self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
+ ) -> Iterable[VersionTypeVar]:
+
+ # Determine if we're forcing a prerelease or not, if we're not forcing
+ # one for this particular filter call, then we'll use whatever the
+ # SpecifierSet thinks for whether or not we should support prereleases.
+ if prereleases is None:
+ prereleases = self.prereleases
+
+ # If we have any specifiers, then we want to wrap our iterable in the
+ # filter method for each one, this will act as a logical AND amongst
+ # each specifier.
+ if self._specs:
+ for spec in self._specs:
+ iterable = spec.filter(iterable, prereleases=bool(prereleases))
+ return iterable
+ # If we do not have any specifiers, then we need to have a rough filter
+ # which will filter out any pre-releases, unless there are no final
+ # releases, and which will filter out LegacyVersion in general.
+ else:
+ filtered: List[VersionTypeVar] = []
+ found_prereleases: List[VersionTypeVar] = []
+
+ item: UnparsedVersion
+ parsed_version: Union[Version, LegacyVersion]
+
+ for item in iterable:
+ # Ensure that we some kind of Version class for this item.
+ if not isinstance(item, (LegacyVersion, Version)):
+ parsed_version = parse(item)
+ else:
+ parsed_version = item
+
+ # Filter out any item which is parsed as a LegacyVersion
+ if isinstance(parsed_version, LegacyVersion):
+ continue
+
+ # Store any item which is a pre-release for later unless we've
+ # already found a final version or we are accepting prereleases
+ if parsed_version.is_prerelease and not prereleases:
+ if not filtered:
+ found_prereleases.append(item)
+ else:
+ filtered.append(item)
+
+ # If we've found no items except for pre-releases, then we'll go
+ # ahead and use the pre-releases
+ if not filtered and found_prereleases and prereleases is None:
+ return found_prereleases
+
+ return filtered
diff --git a/packaging/tags.py b/packaging/tags.py
new file mode 100644
index 0000000..9a3d25a
--- /dev/null
+++ b/packaging/tags.py
@@ -0,0 +1,487 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import logging
+import platform
+import sys
+import sysconfig
+from importlib.machinery import EXTENSION_SUFFIXES
+from typing import (
+ Dict,
+ FrozenSet,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+ cast,
+)
+
+from . import _manylinux, _musllinux
+
+logger = logging.getLogger(__name__)
+
+PythonVersion = Sequence[int]
+MacVersion = Tuple[int, int]
+
+INTERPRETER_SHORT_NAMES: Dict[str, str] = {
+ "python": "py", # Generic.
+ "cpython": "cp",
+ "pypy": "pp",
+ "ironpython": "ip",
+ "jython": "jy",
+}
+
+
+_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32
+
+
+class Tag:
+ """
+ A representation of the tag triple for a wheel.
+
+ Instances are considered immutable and thus are hashable. Equality checking
+ is also supported.
+ """
+
+ __slots__ = ["_interpreter", "_abi", "_platform", "_hash"]
+
+ def __init__(self, interpreter: str, abi: str, platform: str) -> None:
+ self._interpreter = interpreter.lower()
+ self._abi = abi.lower()
+ self._platform = platform.lower()
+ # The __hash__ of every single element in a Set[Tag] will be evaluated each time
+ # that a set calls its `.disjoint()` method, which may be called hundreds of
+ # times when scanning a page of links for packages with tags matching that
+ # Set[Tag]. Pre-computing the value here produces significant speedups for
+ # downstream consumers.
+ self._hash = hash((self._interpreter, self._abi, self._platform))
+
+ @property
+ def interpreter(self) -> str:
+ return self._interpreter
+
+ @property
+ def abi(self) -> str:
+ return self._abi
+
+ @property
+ def platform(self) -> str:
+ return self._platform
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Tag):
+ return NotImplemented
+
+ return (
+ (self._hash == other._hash) # Short-circuit ASAP for perf reasons.
+ and (self._platform == other._platform)
+ and (self._abi == other._abi)
+ and (self._interpreter == other._interpreter)
+ )
+
+ def __hash__(self) -> int:
+ return self._hash
+
+ def __str__(self) -> str:
+ return f"{self._interpreter}-{self._abi}-{self._platform}"
+
+ def __repr__(self) -> str:
+ return f"<{self} @ {id(self)}>"
+
+
+def parse_tag(tag: str) -> FrozenSet[Tag]:
+ """
+ Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
+
+ Returning a set is required due to the possibility that the tag is a
+ compressed tag set.
+ """
+ tags = set()
+ interpreters, abis, platforms = tag.split("-")
+ for interpreter in interpreters.split("."):
+ for abi in abis.split("."):
+ for platform_ in platforms.split("."):
+ tags.add(Tag(interpreter, abi, platform_))
+ return frozenset(tags)
+
+
+def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
+ value = sysconfig.get_config_var(name)
+ if value is None and warn:
+ logger.debug(
+ "Config variable '%s' is unset, Python ABI tag may be incorrect", name
+ )
+ return value
+
+
+def _normalize_string(string: str) -> str:
+ return string.replace(".", "_").replace("-", "_")
+
+
+def _abi3_applies(python_version: PythonVersion) -> bool:
+ """
+ Determine if the Python version supports abi3.
+
+ PEP 384 was first implemented in Python 3.2.
+ """
+ return len(python_version) > 1 and tuple(python_version) >= (3, 2)
+
+
+def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
+ py_version = tuple(py_version) # To allow for version comparison.
+ abis = []
+ version = _version_nodot(py_version[:2])
+ debug = pymalloc = ucs4 = ""
+ with_debug = _get_config_var("Py_DEBUG", warn)
+ has_refcount = hasattr(sys, "gettotalrefcount")
+ # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
+ # extension modules is the best option.
+ # https://github.com/pypa/pip/issues/3383#issuecomment-173267692
+ has_ext = "_d.pyd" in EXTENSION_SUFFIXES
+ if with_debug or (with_debug is None and (has_refcount or has_ext)):
+ debug = "d"
+ if py_version < (3, 8):
+ with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
+ if with_pymalloc or with_pymalloc is None:
+ pymalloc = "m"
+ if py_version < (3, 3):
+ unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
+ if unicode_size == 4 or (
+ unicode_size is None and sys.maxunicode == 0x10FFFF
+ ):
+ ucs4 = "u"
+ elif debug:
+ # Debug builds can also load "normal" extension modules.
+ # We can also assume no UCS-4 or pymalloc requirement.
+ abis.append(f"cp{version}")
+ abis.insert(
+ 0,
+ "cp{version}{debug}{pymalloc}{ucs4}".format(
+ version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4
+ ),
+ )
+ return abis
+
+
+def cpython_tags(
+ python_version: Optional[PythonVersion] = None,
+ abis: Optional[Iterable[str]] = None,
+ platforms: Optional[Iterable[str]] = None,
+ *,
+ warn: bool = False,
+) -> Iterator[Tag]:
+ """
+ Yields the tags for a CPython interpreter.
+
+ The tags consist of:
+ - cp<python_version>-<abi>-<platform>
+ - cp<python_version>-abi3-<platform>
+ - cp<python_version>-none-<platform>
+ - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2.
+
+ If python_version only specifies a major version then user-provided ABIs and
+ the 'none' ABItag will be used.
+
+ If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
+ their normal position and not at the beginning.
+ """
+ if not python_version:
+ python_version = sys.version_info[:2]
+
+ interpreter = f"cp{_version_nodot(python_version[:2])}"
+
+ if abis is None:
+ if len(python_version) > 1:
+ abis = _cpython_abis(python_version, warn)
+ else:
+ abis = []
+ abis = list(abis)
+ # 'abi3' and 'none' are explicitly handled later.
+ for explicit_abi in ("abi3", "none"):
+ try:
+ abis.remove(explicit_abi)
+ except ValueError:
+ pass
+
+ platforms = list(platforms or platform_tags())
+ for abi in abis:
+ for platform_ in platforms:
+ yield Tag(interpreter, abi, platform_)
+ if _abi3_applies(python_version):
+ yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
+ yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
+
+ if _abi3_applies(python_version):
+ for minor_version in range(python_version[1] - 1, 1, -1):
+ for platform_ in platforms:
+ interpreter = "cp{version}".format(
+ version=_version_nodot((python_version[0], minor_version))
+ )
+ yield Tag(interpreter, "abi3", platform_)
+
+
+def _generic_abi() -> Iterator[str]:
+ abi = sysconfig.get_config_var("SOABI")
+ if abi:
+ yield _normalize_string(abi)
+
+
+def generic_tags(
+ interpreter: Optional[str] = None,
+ abis: Optional[Iterable[str]] = None,
+ platforms: Optional[Iterable[str]] = None,
+ *,
+ warn: bool = False,
+) -> Iterator[Tag]:
+ """
+ Yields the tags for a generic interpreter.
+
+ The tags consist of:
+ - <interpreter>-<abi>-<platform>
+
+ The "none" ABI will be added if it was not explicitly provided.
+ """
+ if not interpreter:
+ interp_name = interpreter_name()
+ interp_version = interpreter_version(warn=warn)
+ interpreter = "".join([interp_name, interp_version])
+ if abis is None:
+ abis = _generic_abi()
+ platforms = list(platforms or platform_tags())
+ abis = list(abis)
+ if "none" not in abis:
+ abis.append("none")
+ for abi in abis:
+ for platform_ in platforms:
+ yield Tag(interpreter, abi, platform_)
+
+
+def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
+ """
+ Yields Python versions in descending order.
+
+ After the latest version, the major-only version will be yielded, and then
+ all previous versions of that major version.
+ """
+ if len(py_version) > 1:
+ yield f"py{_version_nodot(py_version[:2])}"
+ yield f"py{py_version[0]}"
+ if len(py_version) > 1:
+ for minor in range(py_version[1] - 1, -1, -1):
+ yield f"py{_version_nodot((py_version[0], minor))}"
+
+
+def compatible_tags(
+ python_version: Optional[PythonVersion] = None,
+ interpreter: Optional[str] = None,
+ platforms: Optional[Iterable[str]] = None,
+) -> Iterator[Tag]:
+ """
+ Yields the sequence of tags that are compatible with a specific version of Python.
+
+ The tags consist of:
+ - py*-none-<platform>
+ - <interpreter>-none-any # ... if `interpreter` is provided.
+ - py*-none-any
+ """
+ if not python_version:
+ python_version = sys.version_info[:2]
+ platforms = list(platforms or platform_tags())
+ for version in _py_interpreter_range(python_version):
+ for platform_ in platforms:
+ yield Tag(version, "none", platform_)
+ if interpreter:
+ yield Tag(interpreter, "none", "any")
+ for version in _py_interpreter_range(python_version):
+ yield Tag(version, "none", "any")
+
+
+def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
+ if not is_32bit:
+ return arch
+
+ if arch.startswith("ppc"):
+ return "ppc"
+
+ return "i386"
+
+
+def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
+ formats = [cpu_arch]
+ if cpu_arch == "x86_64":
+ if version < (10, 4):
+ return []
+ formats.extend(["intel", "fat64", "fat32"])
+
+ elif cpu_arch == "i386":
+ if version < (10, 4):
+ return []
+ formats.extend(["intel", "fat32", "fat"])
+
+ elif cpu_arch == "ppc64":
+ # TODO: Need to care about 32-bit PPC for ppc64 through 10.2?
+ if version > (10, 5) or version < (10, 4):
+ return []
+ formats.append("fat64")
+
+ elif cpu_arch == "ppc":
+ if version > (10, 6):
+ return []
+ formats.extend(["fat32", "fat"])
+
+ if cpu_arch in {"arm64", "x86_64"}:
+ formats.append("universal2")
+
+ if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}:
+ formats.append("universal")
+
+ return formats
+
+
+def mac_platforms(
+ version: Optional[MacVersion] = None, arch: Optional[str] = None
+) -> Iterator[str]:
+ """
+ Yields the platform tags for a macOS system.
+
+ The `version` parameter is a two-item tuple specifying the macOS version to
+ generate platform tags for. The `arch` parameter is the CPU architecture to
+ generate platform tags for. Both parameters default to the appropriate value
+ for the current system.
+ """
+ version_str, _, cpu_arch = platform.mac_ver()
+ if version is None:
+ version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
+ else:
+ version = version
+ if arch is None:
+ arch = _mac_arch(cpu_arch)
+ else:
+ arch = arch
+
+ if (10, 0) <= version and version < (11, 0):
+ # Prior to Mac OS 11, each yearly release of Mac OS bumped the
+ # "minor" version number. The major version was always 10.
+ for minor_version in range(version[1], -1, -1):
+ compat_version = 10, minor_version
+ binary_formats = _mac_binary_formats(compat_version, arch)
+ for binary_format in binary_formats:
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=10, minor=minor_version, binary_format=binary_format
+ )
+
+ if version >= (11, 0):
+ # Starting with Mac OS 11, each yearly release bumps the major version
+ # number. The minor versions are now the midyear updates.
+ for major_version in range(version[0], 10, -1):
+ compat_version = major_version, 0
+ binary_formats = _mac_binary_formats(compat_version, arch)
+ for binary_format in binary_formats:
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=major_version, minor=0, binary_format=binary_format
+ )
+
+ if version >= (11, 0):
+ # Mac OS 11 on x86_64 is compatible with binaries from previous releases.
+ # Arm64 support was introduced in 11.0, so no Arm binaries from previous
+ # releases exist.
+ #
+ # However, the "universal2" binary format can have a
+ # macOS version earlier than 11.0 when the x86_64 part of the binary supports
+ # that version of macOS.
+ if arch == "x86_64":
+ for minor_version in range(16, 3, -1):
+ compat_version = 10, minor_version
+ binary_formats = _mac_binary_formats(compat_version, arch)
+ for binary_format in binary_formats:
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=compat_version[0],
+ minor=compat_version[1],
+ binary_format=binary_format,
+ )
+ else:
+ for minor_version in range(16, 3, -1):
+ compat_version = 10, minor_version
+ binary_format = "universal2"
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=compat_version[0],
+ minor=compat_version[1],
+ binary_format=binary_format,
+ )
+
+
+def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
+ linux = _normalize_string(sysconfig.get_platform())
+ if is_32bit:
+ if linux == "linux_x86_64":
+ linux = "linux_i686"
+ elif linux == "linux_aarch64":
+ linux = "linux_armv7l"
+ _, arch = linux.split("_", 1)
+ yield from _manylinux.platform_tags(linux, arch)
+ yield from _musllinux.platform_tags(arch)
+ yield linux
+
+
+def _generic_platforms() -> Iterator[str]:
+ yield _normalize_string(sysconfig.get_platform())
+
+
+def platform_tags() -> Iterator[str]:
+ """
+ Provides the platform tags for this installation.
+ """
+ if platform.system() == "Darwin":
+ return mac_platforms()
+ elif platform.system() == "Linux":
+ return _linux_platforms()
+ else:
+ return _generic_platforms()
+
+
+def interpreter_name() -> str:
+ """
+ Returns the name of the running interpreter.
+ """
+ name = sys.implementation.name
+ return INTERPRETER_SHORT_NAMES.get(name) or name
+
+
+def interpreter_version(*, warn: bool = False) -> str:
+ """
+ Returns the version of the running interpreter.
+ """
+ version = _get_config_var("py_version_nodot", warn=warn)
+ if version:
+ version = str(version)
+ else:
+ version = _version_nodot(sys.version_info[:2])
+ return version
+
+
+def _version_nodot(version: PythonVersion) -> str:
+ return "".join(map(str, version))
+
+
+def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
+ """
+ Returns the sequence of tag triples for the running interpreter.
+
+ The order of the sequence corresponds to priority order for the
+ interpreter, from most to least important.
+ """
+
+ interp_name = interpreter_name()
+ if interp_name == "cp":
+ yield from cpython_tags(warn=warn)
+ else:
+ yield from generic_tags()
+
+ if interp_name == "pp":
+ yield from compatible_tags(interpreter="pp3")
+ else:
+ yield from compatible_tags()
diff --git a/packaging/utils.py b/packaging/utils.py
new file mode 100644
index 0000000..bab11b8
--- /dev/null
+++ b/packaging/utils.py
@@ -0,0 +1,136 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import re
+from typing import FrozenSet, NewType, Tuple, Union, cast
+
+from .tags import Tag, parse_tag
+from .version import InvalidVersion, Version
+
+BuildTag = Union[Tuple[()], Tuple[int, str]]
+NormalizedName = NewType("NormalizedName", str)
+
+
+class InvalidWheelFilename(ValueError):
+ """
+ An invalid wheel filename was found, users should refer to PEP 427.
+ """
+
+
+class InvalidSdistFilename(ValueError):
+ """
+ An invalid sdist filename was found, users should refer to the packaging user guide.
+ """
+
+
+_canonicalize_regex = re.compile(r"[-_.]+")
+# PEP 427: The build number must start with a digit.
+_build_tag_regex = re.compile(r"(\d+)(.*)")
+
+
+def canonicalize_name(name: str) -> NormalizedName:
+ # This is taken from PEP 503.
+ value = _canonicalize_regex.sub("-", name).lower()
+ return cast(NormalizedName, value)
+
+
+def canonicalize_version(version: Union[Version, str]) -> str:
+ """
+ This is very similar to Version.__str__, but has one subtle difference
+ with the way it handles the release segment.
+ """
+ if isinstance(version, str):
+ try:
+ parsed = Version(version)
+ except InvalidVersion:
+ # Legacy versions cannot be normalized
+ return version
+ else:
+ parsed = version
+
+ parts = []
+
+ # Epoch
+ if parsed.epoch != 0:
+ parts.append(f"{parsed.epoch}!")
+
+ # Release segment
+ # NB: This strips trailing '.0's to normalize
+ parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in parsed.release)))
+
+ # Pre-release
+ if parsed.pre is not None:
+ parts.append("".join(str(x) for x in parsed.pre))
+
+ # Post-release
+ if parsed.post is not None:
+ parts.append(f".post{parsed.post}")
+
+ # Development release
+ if parsed.dev is not None:
+ parts.append(f".dev{parsed.dev}")
+
+ # Local version segment
+ if parsed.local is not None:
+ parts.append(f"+{parsed.local}")
+
+ return "".join(parts)
+
+
+def parse_wheel_filename(
+ filename: str,
+) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:
+ if not filename.endswith(".whl"):
+ raise InvalidWheelFilename(
+ f"Invalid wheel filename (extension must be '.whl'): {filename}"
+ )
+
+ filename = filename[:-4]
+ dashes = filename.count("-")
+ if dashes not in (4, 5):
+ raise InvalidWheelFilename(
+ f"Invalid wheel filename (wrong number of parts): {filename}"
+ )
+
+ parts = filename.split("-", dashes - 2)
+ name_part = parts[0]
+ # See PEP 427 for the rules on escaping the project name
+ if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
+ raise InvalidWheelFilename(f"Invalid project name: {filename}")
+ name = canonicalize_name(name_part)
+ version = Version(parts[1])
+ if dashes == 5:
+ build_part = parts[2]
+ build_match = _build_tag_regex.match(build_part)
+ if build_match is None:
+ raise InvalidWheelFilename(
+ f"Invalid build number: {build_part} in '{filename}'"
+ )
+ build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))
+ else:
+ build = ()
+ tags = parse_tag(parts[-1])
+ return (name, version, build, tags)
+
+
+def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
+ if filename.endswith(".tar.gz"):
+ file_stem = filename[: -len(".tar.gz")]
+ elif filename.endswith(".zip"):
+ file_stem = filename[: -len(".zip")]
+ else:
+ raise InvalidSdistFilename(
+ f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
+ f" {filename}"
+ )
+
+ # We are requiring a PEP 440 version, which cannot contain dashes,
+ # so we split on the last dash.
+ name_part, sep, version_part = file_stem.rpartition("-")
+ if not sep:
+ raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")
+
+ name = canonicalize_name(name_part)
+ version = Version(version_part)
+ return (name, version)
diff --git a/packaging/version.py b/packaging/version.py
new file mode 100644
index 0000000..de9a09a
--- /dev/null
+++ b/packaging/version.py
@@ -0,0 +1,504 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import collections
+import itertools
+import re
+import warnings
+from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
+
+from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
+
+__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
+
+InfiniteTypes = Union[InfinityType, NegativeInfinityType]
+PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
+SubLocalType = Union[InfiniteTypes, int, str]
+LocalType = Union[
+ NegativeInfinityType,
+ Tuple[
+ Union[
+ SubLocalType,
+ Tuple[SubLocalType, str],
+ Tuple[NegativeInfinityType, SubLocalType],
+ ],
+ ...,
+ ],
+]
+CmpKey = Tuple[
+ int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
+]
+LegacyCmpKey = Tuple[int, Tuple[str, ...]]
+VersionComparisonMethod = Callable[
+ [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
+]
+
+_Version = collections.namedtuple(
+ "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
+)
+
+
+def parse(version: str) -> Union["LegacyVersion", "Version"]:
+ """
+ Parse the given version string and return either a :class:`Version` object
+ or a :class:`LegacyVersion` object depending on if the given version is
+ a valid PEP 440 version or a legacy version.
+ """
+ try:
+ return Version(version)
+ except InvalidVersion:
+ return LegacyVersion(version)
+
+
+class InvalidVersion(ValueError):
+ """
+ An invalid version was found, users should refer to PEP 440.
+ """
+
+
+class _BaseVersion:
+ _key: Union[CmpKey, LegacyCmpKey]
+
+ def __hash__(self) -> int:
+ return hash(self._key)
+
+ # Please keep the duplicated `isinstance` check
+ # in the six comparisons hereunder
+ # unless you find a way to avoid adding overhead function calls.
+ def __lt__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key < other._key
+
+ def __le__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key <= other._key
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key == other._key
+
+ def __ge__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key >= other._key
+
+ def __gt__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key > other._key
+
+ def __ne__(self, other: object) -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key != other._key
+
+
+class LegacyVersion(_BaseVersion):
+ def __init__(self, version: str) -> None:
+ self._version = str(version)
+ self._key = _legacy_cmpkey(self._version)
+
+ warnings.warn(
+ "Creating a LegacyVersion has been deprecated and will be "
+ "removed in the next major release",
+ DeprecationWarning,
+ )
+
+ def __str__(self) -> str:
+ return self._version
+
+ def __repr__(self) -> str:
+ return f"<LegacyVersion('{self}')>"
+
+ @property
+ def public(self) -> str:
+ return self._version
+
+ @property
+ def base_version(self) -> str:
+ return self._version
+
+ @property
+ def epoch(self) -> int:
+ return -1
+
+ @property
+ def release(self) -> None:
+ return None
+
+ @property
+ def pre(self) -> None:
+ return None
+
+ @property
+ def post(self) -> None:
+ return None
+
+ @property
+ def dev(self) -> None:
+ return None
+
+ @property
+ def local(self) -> None:
+ return None
+
+ @property
+ def is_prerelease(self) -> bool:
+ return False
+
+ @property
+ def is_postrelease(self) -> bool:
+ return False
+
+ @property
+ def is_devrelease(self) -> bool:
+ return False
+
+
+_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
+
+_legacy_version_replacement_map = {
+ "pre": "c",
+ "preview": "c",
+ "-": "final-",
+ "rc": "c",
+ "dev": "@",
+}
+
+
+def _parse_version_parts(s: str) -> Iterator[str]:
+ for part in _legacy_version_component_re.split(s):
+ part = _legacy_version_replacement_map.get(part, part)
+
+ if not part or part == ".":
+ continue
+
+ if part[:1] in "0123456789":
+ # pad for numeric comparison
+ yield part.zfill(8)
+ else:
+ yield "*" + part
+
+ # ensure that alpha/beta/candidate are before final
+ yield "*final"
+
+
+def _legacy_cmpkey(version: str) -> LegacyCmpKey:
+
+ # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
+ # greater than or equal to 0. This will effectively put the LegacyVersion,
+ # which uses the defacto standard originally implemented by setuptools,
+ # as before all PEP 440 versions.
+ epoch = -1
+
+ # This scheme is taken from pkg_resources.parse_version setuptools prior to
+ # it's adoption of the packaging library.
+ parts: List[str] = []
+ for part in _parse_version_parts(version.lower()):
+ if part.startswith("*"):
+ # remove "-" before a prerelease tag
+ if part < "*final":
+ while parts and parts[-1] == "*final-":
+ parts.pop()
+
+ # remove trailing zeros from each series of numeric parts
+ while parts and parts[-1] == "00000000":
+ parts.pop()
+
+ parts.append(part)
+
+ return epoch, tuple(parts)
+
+
+# Deliberately not anchored to the start and end of the string, to make it
+# easier for 3rd party code to reuse
+VERSION_PATTERN = r"""
+ v?
+ (?:
+ (?:(?P<epoch>[0-9]+)!)? # epoch
+ (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
+ (?P<pre> # pre-release
+ [-_\.]?
+ (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
+ [-_\.]?
+ (?P<pre_n>[0-9]+)?
+ )?
+ (?P<post> # post release
+ (?:-(?P<post_n1>[0-9]+))
+ |
+ (?:
+ [-_\.]?
+ (?P<post_l>post|rev|r)
+ [-_\.]?
+ (?P<post_n2>[0-9]+)?
+ )
+ )?
+ (?P<dev> # dev release
+ [-_\.]?
+ (?P<dev_l>dev)
+ [-_\.]?
+ (?P<dev_n>[0-9]+)?
+ )?
+ )
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
+"""
+
+
+class Version(_BaseVersion):
+
+ _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
+
+ def __init__(self, version: str) -> None:
+
+ # Validate the version and parse it into pieces
+ match = self._regex.search(version)
+ if not match:
+ raise InvalidVersion(f"Invalid version: '{version}'")
+
+ # Store the parsed out pieces of the version
+ self._version = _Version(
+ epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+ release=tuple(int(i) for i in match.group("release").split(".")),
+ pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
+ post=_parse_letter_version(
+ match.group("post_l"), match.group("post_n1") or match.group("post_n2")
+ ),
+ dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
+ local=_parse_local_version(match.group("local")),
+ )
+
+ # Generate a key which will be used for sorting
+ self._key = _cmpkey(
+ self._version.epoch,
+ self._version.release,
+ self._version.pre,
+ self._version.post,
+ self._version.dev,
+ self._version.local,
+ )
+
+ def __repr__(self) -> str:
+ return f"<Version('{self}')>"
+
+ def __str__(self) -> str:
+ parts = []
+
+ # Epoch
+ if self.epoch != 0:
+ parts.append(f"{self.epoch}!")
+
+ # Release segment
+ parts.append(".".join(str(x) for x in self.release))
+
+ # Pre-release
+ if self.pre is not None:
+ parts.append("".join(str(x) for x in self.pre))
+
+ # Post-release
+ if self.post is not None:
+ parts.append(f".post{self.post}")
+
+ # Development release
+ if self.dev is not None:
+ parts.append(f".dev{self.dev}")
+
+ # Local version segment
+ if self.local is not None:
+ parts.append(f"+{self.local}")
+
+ return "".join(parts)
+
+ @property
+ def epoch(self) -> int:
+ _epoch: int = self._version.epoch
+ return _epoch
+
+ @property
+ def release(self) -> Tuple[int, ...]:
+ _release: Tuple[int, ...] = self._version.release
+ return _release
+
+ @property
+ def pre(self) -> Optional[Tuple[str, int]]:
+ _pre: Optional[Tuple[str, int]] = self._version.pre
+ return _pre
+
+ @property
+ def post(self) -> Optional[int]:
+ return self._version.post[1] if self._version.post else None
+
+ @property
+ def dev(self) -> Optional[int]:
+ return self._version.dev[1] if self._version.dev else None
+
+ @property
+ def local(self) -> Optional[str]:
+ if self._version.local:
+ return ".".join(str(x) for x in self._version.local)
+ else:
+ return None
+
+ @property
+ def public(self) -> str:
+ return str(self).split("+", 1)[0]
+
+ @property
+ def base_version(self) -> str:
+ parts = []
+
+ # Epoch
+ if self.epoch != 0:
+ parts.append(f"{self.epoch}!")
+
+ # Release segment
+ parts.append(".".join(str(x) for x in self.release))
+
+ return "".join(parts)
+
+ @property
+ def is_prerelease(self) -> bool:
+ return self.dev is not None or self.pre is not None
+
+ @property
+ def is_postrelease(self) -> bool:
+ return self.post is not None
+
+ @property
+ def is_devrelease(self) -> bool:
+ return self.dev is not None
+
+ @property
+ def major(self) -> int:
+ return self.release[0] if len(self.release) >= 1 else 0
+
+ @property
+ def minor(self) -> int:
+ return self.release[1] if len(self.release) >= 2 else 0
+
+ @property
+ def micro(self) -> int:
+ return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+ letter: str, number: Union[str, bytes, SupportsInt]
+) -> Optional[Tuple[str, int]]:
+
+ if letter:
+ # We consider there to be an implicit 0 in a pre-release if there is
+ # not a numeral associated with it.
+ if number is None:
+ number = 0
+
+ # We normalize any letters to their lower case form
+ letter = letter.lower()
+
+ # We consider some words to be alternate spellings of other words and
+ # in those cases we want to normalize the spellings to our preferred
+ # spelling.
+ if letter == "alpha":
+ letter = "a"
+ elif letter == "beta":
+ letter = "b"
+ elif letter in ["c", "pre", "preview"]:
+ letter = "rc"
+ elif letter in ["rev", "r"]:
+ letter = "post"
+
+ return letter, int(number)
+ if not letter and number:
+ # We assume if we are given a number, but we are not given a letter
+ # then this is using the implicit post release syntax (e.g. 1.0-1)
+ letter = "post"
+
+ return letter, int(number)
+
+ return None
+
+
+_local_version_separators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local: str) -> Optional[LocalType]:
+ """
+ Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+ """
+ if local is not None:
+ return tuple(
+ part.lower() if not part.isdigit() else int(part)
+ for part in _local_version_separators.split(local)
+ )
+ return None
+
+
+def _cmpkey(
+ epoch: int,
+ release: Tuple[int, ...],
+ pre: Optional[Tuple[str, int]],
+ post: Optional[Tuple[str, int]],
+ dev: Optional[Tuple[str, int]],
+ local: Optional[Tuple[SubLocalType]],
+) -> CmpKey:
+
+ # When we compare a release version, we want to compare it with all of the
+ # trailing zeros removed. So we'll use a reverse the list, drop all the now
+ # leading zeros until we come to something non zero, then take the rest
+ # re-reverse it back into the correct order and make it a tuple and use
+ # that for our sorting key.
+ _release = tuple(
+ reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
+ )
+
+ # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+ # We'll do this by abusing the pre segment, but we _only_ want to do this
+ # if there is not a pre or a post segment. If we have one of those then
+ # the normal sorting rules will handle this case correctly.
+ if pre is None and post is None and dev is not None:
+ _pre: PrePostDevType = NegativeInfinity
+ # Versions without a pre-release (except as noted above) should sort after
+ # those with one.
+ elif pre is None:
+ _pre = Infinity
+ else:
+ _pre = pre
+
+ # Versions without a post segment should sort before those with one.
+ if post is None:
+ _post: PrePostDevType = NegativeInfinity
+
+ else:
+ _post = post
+
+ # Versions without a development segment should sort after those with one.
+ if dev is None:
+ _dev: PrePostDevType = Infinity
+
+ else:
+ _dev = dev
+
+ if local is None:
+ # Versions without a local segment should sort before those with one.
+ _local: LocalType = NegativeInfinity
+ else:
+ # Versions with a local segment need that segment parsed to implement
+ # the sorting rules in PEP440.
+ # - Alpha numeric segments sort before numeric segments
+ # - Alpha numeric segments sort lexicographically
+ # - Numeric segments sort numerically
+ # - Shorter versions sort before longer versions when the prefixes
+ # match exactly
+ _local = tuple(
+ (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+ )
+
+ return epoch, _release, _pre, _post, _dev, _local
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..cb37b72
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ['setuptools >= 40.8.0', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..22b4de2
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[isort]
+profile = black
+combine_as_imports = true
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ba1023f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import os
+import re
+
+# While I generally consider it an antipattern to try and support both
+# setuptools and distutils with a single setup.py, in this specific instance
+# where packaging is a dependency of setuptools, it can create a circular
+# dependency when projects attempt to unbundle stuff from setuptools and pip.
+# Though we don't really support that, it makes things easier if we do this and
+# should hopefully cause less issues for end users.
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+
+base_dir = os.path.dirname(__file__)
+
+about = {}
+with open(os.path.join(base_dir, "packaging", "__about__.py")) as f:
+ exec(f.read(), about)
+
+with open(os.path.join(base_dir, "README.rst")) as f:
+ long_description = f.read()
+
+with open(os.path.join(base_dir, "CHANGELOG.rst")) as f:
+ # Remove :issue:`ddd` tags that breaks the description rendering
+ changelog = re.sub(
+ r":issue:`(\d+)`",
+ r"`#\1 <https://github.com/pypa/packaging/issues/\1>`__",
+ f.read(),
+ )
+ long_description = "\n".join([long_description, changelog])
+
+
+setup(
+ name=about["__title__"],
+ version=about["__version__"],
+ description=about["__summary__"],
+ long_description=long_description,
+ long_description_content_type="text/x-rst",
+ license=about["__license__"],
+ url=about["__uri__"],
+ author=about["__author__"],
+ author_email=about["__email__"],
+ python_requires=">=3.6",
+ install_requires=["pyparsing>=2.0.2,!=3.0.5"], # 2.0.2 + needed to avoid issue #91
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ ],
+ packages=["packaging"],
+ package_data={"packaging": ["py.typed"]},
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..b509336
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,3 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
diff --git a/tests/manylinux/hello-world-armv7l-armel b/tests/manylinux/hello-world-armv7l-armel
new file mode 100755
index 0000000..1dfd23f
--- /dev/null
+++ b/tests/manylinux/hello-world-armv7l-armel
Binary files differ
diff --git a/tests/manylinux/hello-world-armv7l-armhf b/tests/manylinux/hello-world-armv7l-armhf
new file mode 100755
index 0000000..965ab30
--- /dev/null
+++ b/tests/manylinux/hello-world-armv7l-armhf
Binary files differ
diff --git a/tests/manylinux/hello-world-invalid-class b/tests/manylinux/hello-world-invalid-class
new file mode 100755
index 0000000..5e9899f
--- /dev/null
+++ b/tests/manylinux/hello-world-invalid-class
Binary files differ
diff --git a/tests/manylinux/hello-world-invalid-data b/tests/manylinux/hello-world-invalid-data
new file mode 100755
index 0000000..2659b8e
--- /dev/null
+++ b/tests/manylinux/hello-world-invalid-data
Binary files differ
diff --git a/tests/manylinux/hello-world-invalid-magic b/tests/manylinux/hello-world-invalid-magic
new file mode 100755
index 0000000..46066ad
--- /dev/null
+++ b/tests/manylinux/hello-world-invalid-magic
Binary files differ
diff --git a/tests/manylinux/hello-world-s390x-s390x b/tests/manylinux/hello-world-s390x-s390x
new file mode 100644
index 0000000..c4e9578
--- /dev/null
+++ b/tests/manylinux/hello-world-s390x-s390x
Binary files differ
diff --git a/tests/manylinux/hello-world-too-short b/tests/manylinux/hello-world-too-short
new file mode 100644
index 0000000..4e5c039
--- /dev/null
+++ b/tests/manylinux/hello-world-too-short
Binary files differ
diff --git a/tests/manylinux/hello-world-x86_64-amd64 b/tests/manylinux/hello-world-x86_64-amd64
new file mode 100644
index 0000000..c7f5b0b
--- /dev/null
+++ b/tests/manylinux/hello-world-x86_64-amd64
Binary files differ
diff --git a/tests/manylinux/hello-world-x86_64-i386 b/tests/manylinux/hello-world-x86_64-i386
new file mode 100755
index 0000000..ff1d540
--- /dev/null
+++ b/tests/manylinux/hello-world-x86_64-i386
Binary files differ
diff --git a/tests/manylinux/hello-world-x86_64-x32 b/tests/manylinux/hello-world-x86_64-x32
new file mode 100755
index 0000000..daf85d3
--- /dev/null
+++ b/tests/manylinux/hello-world-x86_64-x32
Binary files differ
diff --git a/tests/musllinux/glibc-x86_64 b/tests/musllinux/glibc-x86_64
new file mode 100755
index 0000000..59996e2
--- /dev/null
+++ b/tests/musllinux/glibc-x86_64
Binary files differ
diff --git a/tests/musllinux/musl-aarch64 b/tests/musllinux/musl-aarch64
new file mode 100755
index 0000000..f6bcd38
--- /dev/null
+++ b/tests/musllinux/musl-aarch64
Binary files differ
diff --git a/tests/musllinux/musl-i386 b/tests/musllinux/musl-i386
new file mode 100755
index 0000000..2bbe495
--- /dev/null
+++ b/tests/musllinux/musl-i386
Binary files differ
diff --git a/tests/musllinux/musl-x86_64 b/tests/musllinux/musl-x86_64
new file mode 100755
index 0000000..d70261b
--- /dev/null
+++ b/tests/musllinux/musl-x86_64
Binary files differ
diff --git a/tests/test_manylinux.py b/tests/test_manylinux.py
new file mode 100644
index 0000000..a04db15
--- /dev/null
+++ b/tests/test_manylinux.py
@@ -0,0 +1,253 @@
+try:
+ import ctypes
+except ImportError:
+ ctypes = None
+import os
+import platform
+import sys
+import types
+import warnings
+
+import pretend
+import pytest
+
+from packaging import _manylinux
+from packaging._manylinux import (
+ _ELFFileHeader,
+ _get_elf_header,
+ _get_glibc_version,
+ _glibc_version_string,
+ _glibc_version_string_confstr,
+ _glibc_version_string_ctypes,
+ _is_compatible,
+ _is_linux_armhf,
+ _is_linux_i686,
+ _parse_glibc_version,
+)
+
+
+@pytest.fixture(autouse=True)
+def clear_lru_cache():
+ yield
+ _get_glibc_version.cache_clear()
+
+
+@pytest.fixture
+def manylinux_module(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_get_glibc_version", lambda *args: (2, 20))
+ module_name = "_manylinux"
+ module = types.ModuleType(module_name)
+ monkeypatch.setitem(sys.modules, module_name, module)
+ return module
+
+
+@pytest.mark.parametrize("tf", (True, False))
+@pytest.mark.parametrize(
+ "attribute,glibc", (("1", (2, 5)), ("2010", (2, 12)), ("2014", (2, 17)))
+)
+def test_module_declaration(monkeypatch, manylinux_module, attribute, glibc, tf):
+ manylinux = f"manylinux{attribute}_compatible"
+ monkeypatch.setattr(manylinux_module, manylinux, tf, raising=False)
+ res = _is_compatible(manylinux, "x86_64", glibc)
+ assert tf is res
+
+
+@pytest.mark.parametrize(
+ "attribute,glibc", (("1", (2, 5)), ("2010", (2, 12)), ("2014", (2, 17)))
+)
+def test_module_declaration_missing_attribute(
+ monkeypatch, manylinux_module, attribute, glibc
+):
+ manylinux = f"manylinux{attribute}_compatible"
+ monkeypatch.delattr(manylinux_module, manylinux, raising=False)
+ assert _is_compatible(manylinux, "x86_64", glibc)
+
+
+@pytest.mark.parametrize(
+ "version,compatible", (((2, 0), True), ((2, 5), True), ((2, 10), False))
+)
+def test_is_manylinux_compatible_glibc_support(version, compatible, monkeypatch):
+ monkeypatch.setitem(sys.modules, "_manylinux", None)
+ monkeypatch.setattr(_manylinux, "_get_glibc_version", lambda: (2, 5))
+ assert bool(_is_compatible("manylinux1", "any", version)) == compatible
+
+
+@pytest.mark.parametrize("version_str", ["glibc-2.4.5", "2"])
+def test_check_glibc_version_warning(version_str):
+ with warnings.catch_warnings(record=True) as w:
+ _parse_glibc_version(version_str)
+ assert len(w) == 1
+ assert issubclass(w[0].category, RuntimeWarning)
+
+
+@pytest.mark.skipif(not ctypes, reason="requires ctypes")
+@pytest.mark.parametrize(
+ "version_str,expected",
+ [
+ # Be very explicit about bytes and Unicode for Python 2 testing.
+ (b"2.4", "2.4"),
+ ("2.4", "2.4"),
+ ],
+)
+def test_glibc_version_string(version_str, expected, monkeypatch):
+ class LibcVersion:
+ def __init__(self, version_str):
+ self.version_str = version_str
+
+ def __call__(self):
+ return version_str
+
+ class ProcessNamespace:
+ def __init__(self, libc_version):
+ self.gnu_get_libc_version = libc_version
+
+ process_namespace = ProcessNamespace(LibcVersion(version_str))
+ monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace)
+ monkeypatch.setattr(_manylinux, "_glibc_version_string_confstr", lambda: False)
+
+ assert _glibc_version_string() == expected
+
+ del process_namespace.gnu_get_libc_version
+ assert _glibc_version_string() is None
+
+
+def test_glibc_version_string_confstr(monkeypatch):
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ assert _glibc_version_string_confstr() == "2.20"
+
+
+def test_glibc_version_string_fail(monkeypatch):
+ monkeypatch.setattr(os, "confstr", lambda x: None, raising=False)
+ monkeypatch.setitem(sys.modules, "ctypes", None)
+ assert _glibc_version_string() is None
+ assert _get_glibc_version() == (-1, -1)
+
+
+@pytest.mark.parametrize(
+ "failure",
+ [pretend.raiser(ValueError), pretend.raiser(OSError), lambda x: "XXX"],
+)
+def test_glibc_version_string_confstr_fail(monkeypatch, failure):
+ monkeypatch.setattr(os, "confstr", failure, raising=False)
+ assert _glibc_version_string_confstr() is None
+
+
+def test_glibc_version_string_confstr_missing(monkeypatch):
+ monkeypatch.delattr(os, "confstr", raising=False)
+ assert _glibc_version_string_confstr() is None
+
+
+def test_glibc_version_string_ctypes_missing(monkeypatch):
+ monkeypatch.setitem(sys.modules, "ctypes", None)
+ assert _glibc_version_string_ctypes() is None
+
+
+def test_glibc_version_string_ctypes_raise_oserror(monkeypatch):
+ def patched_cdll(name):
+ raise OSError("Dynamic loading not supported")
+
+ monkeypatch.setattr(ctypes, "CDLL", patched_cdll)
+ assert _glibc_version_string_ctypes() is None
+
+
+@pytest.mark.skipif(platform.system() != "Linux", reason="requires Linux")
+def test_is_manylinux_compatible_old():
+ # Assuming no one is running this test with a version of glibc released in
+ # 1997.
+ assert _is_compatible("any", "any", (2, 0))
+
+
+def test_is_manylinux_compatible(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_glibc_version_string", lambda: "2.4")
+ assert _is_compatible("", "any", (2, 4))
+
+
+def test_glibc_version_string_none(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_glibc_version_string", lambda: None)
+ assert not _is_compatible("any", "any", (2, 4))
+
+
+def test_is_linux_armhf_not_elf(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_get_elf_header", lambda: None)
+ assert not _is_linux_armhf()
+
+
+def test_is_linux_i686_not_elf(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_get_elf_header", lambda: None)
+ assert not _is_linux_i686()
+
+
+@pytest.mark.parametrize(
+ "machine, abi, elf_class, elf_data, elf_machine",
+ [
+ (
+ "x86_64",
+ "x32",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_X86_64,
+ ),
+ (
+ "x86_64",
+ "i386",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_386,
+ ),
+ (
+ "x86_64",
+ "amd64",
+ _ELFFileHeader.ELFCLASS64,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_X86_64,
+ ),
+ (
+ "armv7l",
+ "armel",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_ARM,
+ ),
+ (
+ "armv7l",
+ "armhf",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_ARM,
+ ),
+ (
+ "s390x",
+ "s390x",
+ _ELFFileHeader.ELFCLASS64,
+ _ELFFileHeader.ELFDATA2MSB,
+ _ELFFileHeader.EM_S390,
+ ),
+ ],
+)
+def test_get_elf_header(monkeypatch, machine, abi, elf_class, elf_data, elf_machine):
+ path = os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{machine}-{abi}",
+ )
+ monkeypatch.setattr(sys, "executable", path)
+ elf_header = _get_elf_header()
+ assert elf_header.e_ident_class == elf_class
+ assert elf_header.e_ident_data == elf_data
+ assert elf_header.e_machine == elf_machine
+
+
+@pytest.mark.parametrize(
+ "content", [None, "invalid-magic", "invalid-class", "invalid-data", "too-short"]
+)
+def test_get_elf_header_bad_executable(monkeypatch, content):
+ if content:
+ path = os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{content}",
+ )
+ else:
+ path = None
+ monkeypatch.setattr(sys, "executable", path)
+ assert _get_elf_header() is None
diff --git a/tests/test_markers.py b/tests/test_markers.py
new file mode 100644
index 0000000..c2640af
--- /dev/null
+++ b/tests/test_markers.py
@@ -0,0 +1,310 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import collections
+import itertools
+import os
+import platform
+import sys
+
+import pytest
+
+from packaging.markers import (
+ InvalidMarker,
+ Marker,
+ Node,
+ UndefinedComparison,
+ UndefinedEnvironmentName,
+ default_environment,
+ format_full_version,
+)
+
+VARIABLES = [
+ "extra",
+ "implementation_name",
+ "implementation_version",
+ "os_name",
+ "platform_machine",
+ "platform_release",
+ "platform_system",
+ "platform_version",
+ "python_full_version",
+ "python_version",
+ "platform_python_implementation",
+ "sys_platform",
+]
+
+PEP_345_VARIABLES = [
+ "os.name",
+ "sys.platform",
+ "platform.version",
+ "platform.machine",
+ "platform.python_implementation",
+]
+
+SETUPTOOLS_VARIABLES = ["python_implementation"]
+
+OPERATORS = ["===", "==", ">=", "<=", "!=", "~=", ">", "<", "in", "not in"]
+
+VALUES = [
+ "1.0",
+ "5.6a0",
+ "dog",
+ "freebsd",
+ "literally any string can go here",
+ "things @#4 dsfd (((",
+]
+
+
+class TestNode:
+ @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
+ def test_accepts_value(self, value):
+ assert Node(value).value == value
+
+ @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
+ def test_str(self, value):
+ assert str(Node(value)) == str(value)
+
+ @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
+ def test_repr(self, value):
+ assert repr(Node(value)) == f"<Node({str(value)!r})>"
+
+ def test_base_class(self):
+ with pytest.raises(NotImplementedError):
+ Node("cover all the code").serialize()
+
+
+class TestOperatorEvaluation:
+ def test_prefers_pep440(self):
+ assert Marker('"2.7.9" < "foo"').evaluate(dict(foo="2.7.10"))
+
+ def test_falls_back_to_python(self):
+ assert Marker('"b" > "a"').evaluate(dict(a="a"))
+
+ def test_fails_when_undefined(self):
+ with pytest.raises(UndefinedComparison):
+ Marker("'2.7.0' ~= os_name").evaluate()
+
+
+FakeVersionInfo = collections.namedtuple(
+ "FakeVersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]
+)
+
+
+class TestDefaultEnvironment:
+ def test_matches_expected(self):
+ environment = default_environment()
+
+ iver = "{0.major}.{0.minor}.{0.micro}".format(sys.implementation.version)
+ if sys.implementation.version.releaselevel != "final":
+ iver = "{0}{1[0]}{2}".format(
+ iver,
+ sys.implementation.version.releaselevel,
+ sys.implementation.version.serial,
+ )
+
+ assert environment == {
+ "implementation_name": sys.implementation.name,
+ "implementation_version": iver,
+ "os_name": os.name,
+ "platform_machine": platform.machine(),
+ "platform_release": platform.release(),
+ "platform_system": platform.system(),
+ "platform_version": platform.version(),
+ "python_full_version": platform.python_version(),
+ "platform_python_implementation": platform.python_implementation(),
+ "python_version": ".".join(platform.python_version_tuple()[:2]),
+ "sys_platform": sys.platform,
+ }
+
+ def test_multidigit_minor_version(self, monkeypatch):
+ version_info = (3, 10, 0, "final", 0)
+ monkeypatch.setattr(sys, "version_info", version_info, raising=False)
+
+ monkeypatch.setattr(platform, "python_version", lambda: "3.10.0", raising=False)
+ monkeypatch.setattr(
+ platform, "python_version_tuple", lambda: ("3", "10", "0"), raising=False
+ )
+
+ environment = default_environment()
+ assert environment["python_version"] == "3.10"
+
+ def tests_when_releaselevel_final(self):
+ v = FakeVersionInfo(3, 4, 2, "final", 0)
+ assert format_full_version(v) == "3.4.2"
+
+ def tests_when_releaselevel_not_final(self):
+ v = FakeVersionInfo(3, 4, 2, "beta", 4)
+ assert format_full_version(v) == "3.4.2b4"
+
+
+class TestMarker:
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "{} {} {!r}".format(*i)
+ for i in itertools.product(VARIABLES, OPERATORS, VALUES)
+ ]
+ + [
+ "{2!r} {1} {0}".format(*i)
+ for i in itertools.product(VARIABLES, OPERATORS, VALUES)
+ ],
+ )
+ def test_parses_valid(self, marker_string):
+ Marker(marker_string)
+
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "this_isnt_a_real_variable >= '1.0'",
+ "python_version",
+ "(python_version)",
+ "python_version >= 1.0 and (python_version)",
+ ],
+ )
+ def test_parses_invalid(self, marker_string):
+ with pytest.raises(InvalidMarker):
+ Marker(marker_string)
+
+ @pytest.mark.parametrize(
+ ("marker_string", "expected"),
+ [
+ # Test the different quoting rules
+ ("python_version == '2.7'", 'python_version == "2.7"'),
+ ('python_version == "2.7"', 'python_version == "2.7"'),
+ # Test and/or expressions
+ (
+ 'python_version == "2.7" and os_name == "linux"',
+ 'python_version == "2.7" and os_name == "linux"',
+ ),
+ (
+ 'python_version == "2.7" or os_name == "linux"',
+ 'python_version == "2.7" or os_name == "linux"',
+ ),
+ (
+ 'python_version == "2.7" and os_name == "linux" or '
+ 'sys_platform == "win32"',
+ 'python_version == "2.7" and os_name == "linux" or '
+ 'sys_platform == "win32"',
+ ),
+ # Test nested expressions and grouping with ()
+ ('(python_version == "2.7")', 'python_version == "2.7"'),
+ (
+ '(python_version == "2.7" and sys_platform == "win32")',
+ 'python_version == "2.7" and sys_platform == "win32"',
+ ),
+ (
+ 'python_version == "2.7" and (sys_platform == "win32" or '
+ 'sys_platform == "linux")',
+ 'python_version == "2.7" and (sys_platform == "win32" or '
+ 'sys_platform == "linux")',
+ ),
+ ],
+ )
+ def test_str_and_repr(self, marker_string, expected):
+ m = Marker(marker_string)
+ assert str(m) == expected
+ assert repr(m) == f"<Marker({str(m)!r})>"
+
+ def test_extra_with_no_extra_in_environment(self):
+ # We can't evaluate an extra if no extra is passed into the environment
+ m = Marker("extra == 'security'")
+ with pytest.raises(UndefinedEnvironmentName):
+ m.evaluate()
+
+ @pytest.mark.parametrize(
+ ("marker_string", "environment", "expected"),
+ [
+ (f"os_name == '{os.name}'", None, True),
+ ("os_name == 'foo'", {"os_name": "foo"}, True),
+ ("os_name == 'foo'", {"os_name": "bar"}, False),
+ ("'2.7' in python_version", {"python_version": "2.7.5"}, True),
+ ("'2.7' not in python_version", {"python_version": "2.7"}, False),
+ (
+ "os_name == 'foo' and python_version ~= '2.7.0'",
+ {"os_name": "foo", "python_version": "2.7.6"},
+ True,
+ ),
+ (
+ "python_version ~= '2.7.0' and (os_name == 'foo' or "
+ "os_name == 'bar')",
+ {"os_name": "foo", "python_version": "2.7.4"},
+ True,
+ ),
+ (
+ "python_version ~= '2.7.0' and (os_name == 'foo' or "
+ "os_name == 'bar')",
+ {"os_name": "bar", "python_version": "2.7.4"},
+ True,
+ ),
+ (
+ "python_version ~= '2.7.0' and (os_name == 'foo' or "
+ "os_name == 'bar')",
+ {"os_name": "other", "python_version": "2.7.4"},
+ False,
+ ),
+ ("extra == 'security'", {"extra": "quux"}, False),
+ ("extra == 'security'", {"extra": "security"}, True),
+ ],
+ )
+ def test_evaluates(self, marker_string, environment, expected):
+ args = [] if environment is None else [environment]
+ assert Marker(marker_string).evaluate(*args) == expected
+
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "{} {} {!r}".format(*i)
+ for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
+ ]
+ + [
+ "{2!r} {1} {0}".format(*i)
+ for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
+ ],
+ )
+ def test_parses_pep345_valid(self, marker_string):
+ Marker(marker_string)
+
+ @pytest.mark.parametrize(
+ ("marker_string", "environment", "expected"),
+ [
+ (f"os.name == '{os.name}'", None, True),
+ ("sys.platform == 'win32'", {"sys_platform": "linux2"}, False),
+ ("platform.version in 'Ubuntu'", {"platform_version": "#39"}, False),
+ ("platform.machine=='x86_64'", {"platform_machine": "x86_64"}, True),
+ (
+ "platform.python_implementation=='Jython'",
+ {"platform_python_implementation": "CPython"},
+ False,
+ ),
+ (
+ "python_version == '2.5' and platform.python_implementation"
+ "!= 'Jython'",
+ {"python_version": "2.7"},
+ False,
+ ),
+ ],
+ )
+ def test_evaluate_pep345_markers(self, marker_string, environment, expected):
+ args = [] if environment is None else [environment]
+ assert Marker(marker_string).evaluate(*args) == expected
+
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "{} {} {!r}".format(*i)
+ for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
+ ]
+ + [
+ "{2!r} {1} {0}".format(*i)
+ for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
+ ],
+ )
+ def test_parses_setuptools_legacy_valid(self, marker_string):
+ Marker(marker_string)
+
+ def test_evaluate_setuptools_legacy_markers(self):
+ marker_string = "python_implementation=='Jython'"
+ args = [{"platform_python_implementation": "CPython"}]
+ assert Marker(marker_string).evaluate(*args) is False
diff --git a/tests/test_musllinux.py b/tests/test_musllinux.py
new file mode 100644
index 0000000..d2c87ca
--- /dev/null
+++ b/tests/test_musllinux.py
@@ -0,0 +1,146 @@
+import collections
+import io
+import pathlib
+import struct
+import subprocess
+
+import pretend
+import pytest
+
+from packaging import _musllinux
+from packaging._musllinux import (
+ _get_musl_version,
+ _MuslVersion,
+ _parse_ld_musl_from_elf,
+ _parse_musl_version,
+)
+
+MUSL_AMD64 = "musl libc (x86_64)\nVersion 1.2.2\n"
+MUSL_I386 = "musl libc (i386)\nVersion 1.2.1\n"
+MUSL_AARCH64 = "musl libc (aarch64)\nVersion 1.1.24\n"
+MUSL_INVALID = "musl libc (invalid)\n"
+MUSL_UNKNOWN = "musl libc (unknown)\nVersion unknown\n"
+
+MUSL_DIR = pathlib.Path(__file__).with_name("musllinux").resolve()
+
+BIN_GLIBC_X86_64 = MUSL_DIR.joinpath("glibc-x86_64")
+BIN_MUSL_X86_64 = MUSL_DIR.joinpath("musl-x86_64")
+BIN_MUSL_I386 = MUSL_DIR.joinpath("musl-i386")
+BIN_MUSL_AARCH64 = MUSL_DIR.joinpath("musl-aarch64")
+
+LD_MUSL_X86_64 = "/lib/ld-musl-x86_64.so.1"
+LD_MUSL_I386 = "/lib/ld-musl-i386.so.1"
+LD_MUSL_AARCH64 = "/lib/ld-musl-aarch64.so.1"
+
+
+@pytest.fixture(autouse=True)
+def clear_lru_cache():
+ yield
+ _get_musl_version.cache_clear()
+
+
+@pytest.mark.parametrize(
+ "output, version",
+ [
+ (MUSL_AMD64, _MuslVersion(1, 2)),
+ (MUSL_I386, _MuslVersion(1, 2)),
+ (MUSL_AARCH64, _MuslVersion(1, 1)),
+ (MUSL_INVALID, None),
+ (MUSL_UNKNOWN, None),
+ ],
+ ids=["amd64-1.2.2", "i386-1.2.1", "aarch64-1.1.24", "invalid", "unknown"],
+)
+def test_parse_musl_version(output, version):
+ assert _parse_musl_version(output) == version
+
+
+@pytest.mark.parametrize(
+ "executable, location",
+ [
+ (BIN_GLIBC_X86_64, None),
+ (BIN_MUSL_X86_64, LD_MUSL_X86_64),
+ (BIN_MUSL_I386, LD_MUSL_I386),
+ (BIN_MUSL_AARCH64, LD_MUSL_AARCH64),
+ ],
+ ids=["glibc", "x86_64", "i386", "aarch64"],
+)
+def test_parse_ld_musl_from_elf(executable, location):
+ with executable.open("rb") as f:
+ assert _parse_ld_musl_from_elf(f) == location
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ # Too short for magic.
+ b"\0",
+ # Enough for magic, but not ELF.
+ b"#!/bin/bash" + b"\0" * 16,
+ # ELF, but unknown byte declaration.
+ b"\x7fELF\3" + b"\0" * 16,
+ ],
+ ids=["no-magic", "wrong-magic", "unknown-format"],
+)
+def test_parse_ld_musl_from_elf_invalid(data):
+ assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None
+
+
+@pytest.mark.parametrize(
+ "head",
+ [
+ 25, # Enough for magic, but not the section definitions.
+ 58, # Enough for section definitions, but not the actual sections.
+ ],
+)
+def test_parse_ld_musl_from_elf_invalid_section(head):
+ data = BIN_MUSL_X86_64.read_bytes()[:head]
+ assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None
+
+
+def test_parse_ld_musl_from_elf_no_interpreter_section():
+ with BIN_MUSL_X86_64.open("rb") as f:
+ data = f.read()
+
+ # Change all sections to *not* PT_INTERP.
+ unpacked = struct.unpack("16BHHIQQQIHHH", data[:58])
+ *_, e_phoff, _, _, _, e_phentsize, e_phnum = unpacked
+ for i in range(e_phnum + 1):
+ sb = e_phoff + e_phentsize * i
+ se = sb + 56
+ section = struct.unpack("IIQQQQQQ", data[sb:se])
+ data = data[:sb] + struct.pack("IIQQQQQQ", 0, *section[1:]) + data[se:]
+
+ assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None
+
+
+@pytest.mark.parametrize(
+ "executable, output, version, ld_musl",
+ [
+ (MUSL_DIR.joinpath("does-not-exist"), "error", None, None),
+ (BIN_GLIBC_X86_64, "error", None, None),
+ (BIN_MUSL_X86_64, MUSL_AMD64, _MuslVersion(1, 2), LD_MUSL_X86_64),
+ (BIN_MUSL_I386, MUSL_I386, _MuslVersion(1, 2), LD_MUSL_I386),
+ (BIN_MUSL_AARCH64, MUSL_AARCH64, _MuslVersion(1, 1), LD_MUSL_AARCH64),
+ ],
+ ids=["does-not-exist", "glibc", "x86_64", "i386", "aarch64"],
+)
+def test_get_musl_version(monkeypatch, executable, output, version, ld_musl):
+ def mock_run(*args, **kwargs):
+ return collections.namedtuple("Proc", "stderr")(output)
+
+ run_recorder = pretend.call_recorder(mock_run)
+ monkeypatch.setattr(_musllinux.subprocess, "run", run_recorder)
+
+ assert _get_musl_version(str(executable)) == version
+
+ if ld_musl is not None:
+ expected_calls = [
+ pretend.call(
+ [ld_musl],
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ )
+ ]
+ else:
+ expected_calls = []
+ assert run_recorder.calls == expected_calls
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
new file mode 100644
index 0000000..f2c209c
--- /dev/null
+++ b/tests/test_requirements.py
@@ -0,0 +1,197 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import pytest
+
+from packaging.markers import Marker
+from packaging.requirements import URL, URL_AND_MARKER, InvalidRequirement, Requirement
+from packaging.specifiers import SpecifierSet
+
+
+class TestRequirements:
+ def test_string_specifier_marker(self):
+ requirement = 'name[bar]>=3; python_version == "2.7"'
+ req = Requirement(requirement)
+ assert str(req) == requirement
+
+ def test_string_url(self):
+ requirement = "name@ http://foo.com"
+ req = Requirement(requirement)
+ assert str(req) == requirement
+
+ def test_string_url_with_marker(self):
+ requirement = 'name@ http://foo.com ; extra == "feature"'
+ req = Requirement(requirement)
+ assert str(req) == requirement
+
+ def test_repr(self):
+ req = Requirement("name")
+ assert repr(req) == "<Requirement('name')>"
+
+ def _assert_requirement(
+ self, req, name, url=None, extras=[], specifier="", marker=None
+ ):
+ assert req.name == name
+ assert req.url == url
+ assert sorted(req.extras) == sorted(extras)
+ assert str(req.specifier) == specifier
+ if marker:
+ assert str(req.marker) == marker
+ else:
+ assert req.marker is None
+
+ def test_simple_names(self):
+ for name in ("A", "aa", "name"):
+ req = Requirement(name)
+ self._assert_requirement(req, name)
+
+ def test_name_with_other_characters(self):
+ name = "foo-bar.quux_baz"
+ req = Requirement(name)
+ self._assert_requirement(req, name)
+
+ def test_invalid_name(self):
+ with pytest.raises(InvalidRequirement):
+ Requirement("foo!")
+
+ def test_name_with_version(self):
+ req = Requirement("name>=3")
+ self._assert_requirement(req, "name", specifier=">=3")
+
+ def test_with_legacy_version(self):
+ req = Requirement("name==1.0.org1")
+ self._assert_requirement(req, "name", specifier="==1.0.org1")
+
+ def test_with_legacy_version_and_marker(self):
+ req = Requirement("name>=1.x.y;python_version=='2.6'")
+ self._assert_requirement(
+ req, "name", specifier=">=1.x.y", marker='python_version == "2.6"'
+ )
+
+ def test_version_with_parens_and_whitespace(self):
+ req = Requirement("name (==4)")
+ self._assert_requirement(req, "name", specifier="==4")
+
+ def test_name_with_multiple_versions(self):
+ req = Requirement("name>=3,<2")
+ self._assert_requirement(req, "name", specifier="<2,>=3")
+
+ def test_name_with_multiple_versions_and_whitespace(self):
+ req = Requirement("name >=2, <3")
+ self._assert_requirement(req, "name", specifier="<3,>=2")
+
+ def test_extras(self):
+ req = Requirement("foobar [quux,bar]")
+ self._assert_requirement(req, "foobar", extras=["bar", "quux"])
+
+ def test_empty_extras(self):
+ req = Requirement("foo[]")
+ self._assert_requirement(req, "foo")
+
+ def test_url(self):
+ url_section = "@ http://example.com"
+ parsed = URL.parseString(url_section)
+ assert parsed.url == "http://example.com"
+
+ def test_url_and_marker(self):
+ instring = "@ http://example.com ; os_name=='a'"
+ parsed = URL_AND_MARKER.parseString(instring)
+ assert parsed.url == "http://example.com"
+ assert str(parsed.marker) == 'os_name == "a"'
+
+ def test_invalid_url(self):
+ with pytest.raises(InvalidRequirement) as e:
+ Requirement("name @ gopher:/foo/com")
+ assert "Invalid URL: " in str(e.value)
+ assert "gopher:/foo/com" in str(e.value)
+
+ def test_file_url(self):
+ req = Requirement("name @ file:///absolute/path")
+ self._assert_requirement(req, "name", "file:///absolute/path")
+ req = Requirement("name @ file://.")
+ self._assert_requirement(req, "name", "file://.")
+
+ def test_invalid_file_urls(self):
+ with pytest.raises(InvalidRequirement):
+ Requirement("name @ file:.")
+ with pytest.raises(InvalidRequirement):
+ Requirement("name @ file:/.")
+
+ def test_extras_and_url_and_marker(self):
+ req = Requirement("name [fred,bar] @ http://foo.com ; python_version=='2.7'")
+ self._assert_requirement(
+ req,
+ "name",
+ extras=["bar", "fred"],
+ url="http://foo.com",
+ marker='python_version == "2.7"',
+ )
+
+ def test_complex_url_and_marker(self):
+ url = "https://example.com/name;v=1.1/?query=foo&bar=baz#blah"
+ req = Requirement("foo @ %s ; python_version=='3.4'" % url)
+ self._assert_requirement(req, "foo", url=url, marker='python_version == "3.4"')
+
+ def test_multiple_markers(self):
+ req = Requirement(
+ "name[quux, strange];python_version<'2.7' and " "platform_version=='2'"
+ )
+ marker = 'python_version < "2.7" and platform_version == "2"'
+ self._assert_requirement(req, "name", extras=["strange", "quux"], marker=marker)
+
+ def test_multiple_comparison_markers(self):
+ req = Requirement("name; os_name=='a' and os_name=='b' or os_name=='c'")
+ marker = 'os_name == "a" and os_name == "b" or os_name == "c"'
+ self._assert_requirement(req, "name", marker=marker)
+
+ def test_invalid_marker(self):
+ with pytest.raises(InvalidRequirement):
+ Requirement("name; foobar=='x'")
+
+ def test_types(self):
+ req = Requirement("foobar[quux]<2,>=3; os_name=='a'")
+ assert isinstance(req.name, str)
+ assert isinstance(req.extras, set)
+ assert req.url is None
+ assert isinstance(req.specifier, SpecifierSet)
+ assert isinstance(req.marker, Marker)
+
+ def test_types_with_nothing(self):
+ req = Requirement("foobar")
+ assert isinstance(req.name, str)
+ assert isinstance(req.extras, set)
+ assert req.url is None
+ assert isinstance(req.specifier, SpecifierSet)
+ assert req.marker is None
+
+ def test_types_with_url(self):
+ req = Requirement("foobar @ http://foo.com")
+ assert isinstance(req.name, str)
+ assert isinstance(req.extras, set)
+ assert isinstance(req.url, str)
+ assert isinstance(req.specifier, SpecifierSet)
+ assert req.marker is None
+
+ def test_sys_platform_linux_equal(self):
+ req = Requirement('something>=1.2.3; sys_platform == "foo"')
+
+ assert req.name == "something"
+ assert req.marker is not None
+ assert req.marker.evaluate(dict(sys_platform="foo")) is True
+ assert req.marker.evaluate(dict(sys_platform="bar")) is False
+
+ def test_sys_platform_linux_in(self):
+ req = Requirement("aviato>=1.2.3; 'f' in sys_platform")
+
+ assert req.name == "aviato"
+ assert req.marker is not None
+ assert req.marker.evaluate(dict(sys_platform="foo")) is True
+ assert req.marker.evaluate(dict(sys_platform="bar")) is False
+
+ def test_parseexception_error_msg(self):
+ with pytest.raises(InvalidRequirement) as e:
+ Requirement("toto 42")
+ assert "Expected stringEnd" in str(e.value) or (
+ "Expected string_end" in str(e.value) # pyparsing>=3.0.0
+ )
diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py
new file mode 100644
index 0000000..ca21fa1
--- /dev/null
+++ b/tests/test_specifiers.py
@@ -0,0 +1,998 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import itertools
+import operator
+import warnings
+
+import pytest
+
+from packaging.specifiers import (
+ InvalidSpecifier,
+ LegacySpecifier,
+ Specifier,
+ SpecifierSet,
+)
+from packaging.version import LegacyVersion, Version, parse
+
+from .test_version import LEGACY_VERSIONS, VERSIONS
+
+LEGACY_SPECIFIERS = [
+ "==2.1.0.3",
+ "!=2.2.0.5",
+ "<=5",
+ ">=7.9a1",
+ "<1.0.dev1",
+ ">2.0.post1",
+]
+
+SPECIFIERS = [
+ "~=2.0",
+ "==2.1.*",
+ "==2.1.0.3",
+ "!=2.2.*",
+ "!=2.2.0.5",
+ "<=5",
+ ">=7.9a1",
+ "<1.0.dev1",
+ ">2.0.post1",
+ "===lolwat",
+]
+
+
+class TestSpecifier:
+ @pytest.mark.parametrize("specifier", SPECIFIERS)
+ def test_specifiers_valid(self, specifier):
+ Specifier(specifier)
+
+ @pytest.mark.parametrize(
+ "specifier",
+ [
+ # Operator-less specifier
+ "2.0",
+ # Invalid operator
+ "=>2.0",
+ # Version-less specifier
+ "==",
+ # Local segment on operators which don't support them
+ "~=1.0+5",
+ ">=1.0+deadbeef",
+ "<=1.0+abc123",
+ ">1.0+watwat",
+ "<1.0+1.0",
+ # Prefix matching on operators which don't support them
+ "~=1.0.*",
+ ">=1.0.*",
+ "<=1.0.*",
+ ">1.0.*",
+ "<1.0.*",
+ # Combination of local and prefix matching on operators which do
+ # support one or the other
+ "==1.0.*+5",
+ "!=1.0.*+deadbeef",
+ # Prefix matching cannot be used inside of a local version
+ "==1.0+5.*",
+ "!=1.0+deadbeef.*",
+ # Prefix matching must appear at the end
+ "==1.0.*.5",
+ # Compatible operator requires 2 digits in the release operator
+ "~=1",
+ # Cannot use a prefix matching after a .devN version
+ "==1.0.dev1.*",
+ "!=1.0.dev1.*",
+ ],
+ )
+ def test_specifiers_invalid(self, specifier):
+ with pytest.raises(InvalidSpecifier):
+ Specifier(specifier)
+
+ @pytest.mark.parametrize(
+ "version",
+ [
+ # Various development release incarnations
+ "1.0dev",
+ "1.0.dev",
+ "1.0dev1",
+ "1.0dev",
+ "1.0-dev",
+ "1.0-dev1",
+ "1.0DEV",
+ "1.0.DEV",
+ "1.0DEV1",
+ "1.0DEV",
+ "1.0.DEV1",
+ "1.0-DEV",
+ "1.0-DEV1",
+ # Various alpha incarnations
+ "1.0a",
+ "1.0.a",
+ "1.0.a1",
+ "1.0-a",
+ "1.0-a1",
+ "1.0alpha",
+ "1.0.alpha",
+ "1.0.alpha1",
+ "1.0-alpha",
+ "1.0-alpha1",
+ "1.0A",
+ "1.0.A",
+ "1.0.A1",
+ "1.0-A",
+ "1.0-A1",
+ "1.0ALPHA",
+ "1.0.ALPHA",
+ "1.0.ALPHA1",
+ "1.0-ALPHA",
+ "1.0-ALPHA1",
+ # Various beta incarnations
+ "1.0b",
+ "1.0.b",
+ "1.0.b1",
+ "1.0-b",
+ "1.0-b1",
+ "1.0beta",
+ "1.0.beta",
+ "1.0.beta1",
+ "1.0-beta",
+ "1.0-beta1",
+ "1.0B",
+ "1.0.B",
+ "1.0.B1",
+ "1.0-B",
+ "1.0-B1",
+ "1.0BETA",
+ "1.0.BETA",
+ "1.0.BETA1",
+ "1.0-BETA",
+ "1.0-BETA1",
+ # Various release candidate incarnations
+ "1.0c",
+ "1.0.c",
+ "1.0.c1",
+ "1.0-c",
+ "1.0-c1",
+ "1.0rc",
+ "1.0.rc",
+ "1.0.rc1",
+ "1.0-rc",
+ "1.0-rc1",
+ "1.0C",
+ "1.0.C",
+ "1.0.C1",
+ "1.0-C",
+ "1.0-C1",
+ "1.0RC",
+ "1.0.RC",
+ "1.0.RC1",
+ "1.0-RC",
+ "1.0-RC1",
+ # Various post release incarnations
+ "1.0post",
+ "1.0.post",
+ "1.0post1",
+ "1.0post",
+ "1.0-post",
+ "1.0-post1",
+ "1.0POST",
+ "1.0.POST",
+ "1.0POST1",
+ "1.0POST",
+ "1.0.POST1",
+ "1.0-POST",
+ "1.0-POST1",
+ "1.0-5",
+ # Local version case insensitivity
+ "1.0+AbC"
+ # Integer Normalization
+ "1.01",
+ "1.0a05",
+ "1.0b07",
+ "1.0c056",
+ "1.0rc09",
+ "1.0.post000",
+ "1.1.dev09000",
+ "00!1.2",
+ "0100!0.0",
+ # Various other normalizations
+ "v1.0",
+ " \r \f \v v1.0\t\n",
+ ],
+ )
+ def test_specifiers_normalized(self, version):
+ if "+" not in version:
+ ops = ["~=", "==", "!=", "<=", ">=", "<", ">"]
+ else:
+ ops = ["==", "!="]
+
+ for op in ops:
+ Specifier(op + version)
+
+ @pytest.mark.parametrize(
+ ("specifier", "expected"),
+ [
+ # Single item specifiers should just be reflexive
+ ("!=2.0", "!=2.0"),
+ ("<2.0", "<2.0"),
+ ("<=2.0", "<=2.0"),
+ ("==2.0", "==2.0"),
+ (">2.0", ">2.0"),
+ (">=2.0", ">=2.0"),
+ ("~=2.0", "~=2.0"),
+ # Spaces should be removed
+ ("< 2", "<2"),
+ ],
+ )
+ def test_specifiers_str_and_repr(self, specifier, expected):
+ spec = Specifier(specifier)
+
+ assert str(spec) == expected
+ assert repr(spec) == f"<Specifier({expected!r})>"
+
+ @pytest.mark.parametrize("specifier", SPECIFIERS)
+ def test_specifiers_hash(self, specifier):
+ assert hash(Specifier(specifier)) == hash(Specifier(specifier))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.ne) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(Specifier(left), Specifier(right))
+ assert op(left, Specifier(right))
+ assert op(Specifier(left), right)
+
+ @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")])
+ def test_comparison_canonicalizes(self, left, right):
+ assert Specifier(left) == Specifier(right)
+ assert left == Specifier(right)
+ assert Specifier(left) == right
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.ne) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.eq) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(Specifier(left), Specifier(right))
+ assert not op(left, Specifier(right))
+ assert not op(Specifier(left), right)
+
+ def test_comparison_non_specifier(self):
+ assert Specifier("==1.0") != 12
+ assert not Specifier("==1.0") == 12
+ assert Specifier("==1.0") != "12"
+ assert not Specifier("==1.0") == "12"
+
+ @pytest.mark.parametrize(
+ ("version", "spec", "expected"),
+ [
+ (v, s, True)
+ for v, s in [
+ # 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.*"),
+ ("2rc1", "==2.*"),
+ ("2", "==2.*"),
+ ("2.0", "==2.*"),
+ ("2.0.0", "==2.*"),
+ ("2.0.post1", "==2.0.post1.*"),
+ ("2.0.post1.dev1", "==2.0.post1.*"),
+ ("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"),
+ # 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"),
+ ]
+ ]
+ + [
+ (v, s, False)
+ for v, s in [
+ # 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.*"),
+ ("2rc1", "!=2.*"),
+ ("2", "!=2.*"),
+ ("2.0", "!=2.*"),
+ ("2.0.0", "!=2.*"),
+ ("2.0.post1", "!=2.0.post1.*"),
+ ("2.0.post1.dev1", "!=2.0.post1.*"),
+ # 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"),
+ ]
+ ],
+ )
+ def test_specifiers(self, version, spec, expected):
+ spec = Specifier(spec, prereleases=True)
+
+ if expected:
+ # Test that the plain string form works
+ assert version in spec
+ assert spec.contains(version)
+
+ # Test that the version instance form works
+ assert Version(version) in spec
+ assert spec.contains(Version(version))
+ else:
+ # Test that the plain string form works
+ assert version not in spec
+ assert not spec.contains(version)
+
+ # Test that the version instance form works
+ assert Version(version) not in spec
+ assert not spec.contains(Version(version))
+
+ @pytest.mark.parametrize(
+ ("version", "spec", "expected"),
+ [
+ # Test identity comparison by itself
+ ("lolwat", "===lolwat", True),
+ ("Lolwat", "===lolwat", True),
+ ("1.0", "===1.0", True),
+ ("nope", "===lolwat", False),
+ ("1.0.0", "===1.0", False),
+ ("1.0.dev0", "===1.0.dev0", True),
+ ],
+ )
+ def test_specifiers_identity(self, version, spec, expected):
+ spec = Specifier(spec)
+
+ if expected:
+ # Identity comparisons only support the plain string form
+ assert version in spec
+ else:
+ # Identity comparisons only support the plain string form
+ assert version not in spec
+
+ @pytest.mark.parametrize(
+ ("specifier", "expected"),
+ [
+ ("==1.0", False),
+ (">=1.0", False),
+ ("<=1.0", False),
+ ("~=1.0", False),
+ ("<1.0", False),
+ (">1.0", False),
+ ("<1.0.dev1", False),
+ (">1.0.dev1", False),
+ ("==1.0.*", False),
+ ("==1.0.dev1", True),
+ (">=1.0.dev1", True),
+ ("<=1.0.dev1", True),
+ ("~=1.0.dev1", True),
+ ],
+ )
+ def test_specifier_prereleases_detection(self, specifier, expected):
+ assert Specifier(specifier).prereleases == expected
+
+ @pytest.mark.parametrize(
+ ("specifier", "version", "expected"),
+ [
+ (">=1.0", "2.0.dev1", False),
+ (">=2.0.dev1", "2.0a1", True),
+ ("==2.0.*", "2.0a1.dev1", False),
+ ("==2.0a1.*", "2.0a1.dev1", True),
+ ("<=2.0", "1.0.dev1", False),
+ ("<=2.0.dev1", "1.0a1", True),
+ ],
+ )
+ def test_specifiers_prereleases(self, specifier, version, expected):
+ spec = Specifier(specifier)
+
+ if expected:
+ assert version in spec
+ spec.prereleases = False
+ assert version not in spec
+ else:
+ assert version not in spec
+ spec.prereleases = True
+ assert version in spec
+
+ @pytest.mark.parametrize(
+ ("specifier", "prereleases", "input", "expected"),
+ [
+ (">=1.0", None, ["2.0a1"], ["2.0a1"]),
+ (">=1.0.dev1", None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ (">=1.0.dev1", False, ["1.0", "2.0a1"], ["1.0"]),
+ ],
+ )
+ def test_specifier_filter(self, specifier, prereleases, input, expected):
+ spec = Specifier(specifier)
+
+ kwargs = {"prereleases": prereleases} if prereleases is not None else {}
+
+ assert list(spec.filter(input, **kwargs)) == expected
+
+ @pytest.mark.xfail
+ def test_specifier_explicit_legacy(self):
+ assert Specifier("==1.0").contains(LegacyVersion("1.0"))
+
+ @pytest.mark.parametrize(
+ ("spec", "op"),
+ [
+ ("~=2.0", "~="),
+ ("==2.1.*", "=="),
+ ("==2.1.0.3", "=="),
+ ("!=2.2.*", "!="),
+ ("!=2.2.0.5", "!="),
+ ("<=5", "<="),
+ (">=7.9a1", ">="),
+ ("<1.0.dev1", "<"),
+ (">2.0.post1", ">"),
+ ("===lolwat", "==="),
+ ],
+ )
+ def test_specifier_operator_property(self, spec, op):
+ assert Specifier(spec).operator == op
+
+ @pytest.mark.parametrize(
+ ("spec", "version"),
+ [
+ ("~=2.0", "2.0"),
+ ("==2.1.*", "2.1.*"),
+ ("==2.1.0.3", "2.1.0.3"),
+ ("!=2.2.*", "2.2.*"),
+ ("!=2.2.0.5", "2.2.0.5"),
+ ("<=5", "5"),
+ (">=7.9a1", "7.9a1"),
+ ("<1.0.dev1", "1.0.dev1"),
+ (">2.0.post1", "2.0.post1"),
+ ("===lolwat", "lolwat"),
+ ],
+ )
+ def test_specifier_version_property(self, spec, version):
+ assert Specifier(spec).version == version
+
+ @pytest.mark.parametrize(
+ ("spec", "expected_length"),
+ [("", 0), ("==2.0", 1), (">=2.0", 1), (">=2.0,<3", 2), (">=2.0,<3,==2.4", 3)],
+ )
+ def test_length(self, spec, expected_length):
+ spec = SpecifierSet(spec)
+ assert len(spec) == expected_length
+
+ @pytest.mark.parametrize(
+ ("spec", "expected_items"),
+ [
+ ("", []),
+ ("==2.0", ["==2.0"]),
+ (">=2.0", [">=2.0"]),
+ (">=2.0,<3", [">=2.0", "<3"]),
+ (">=2.0,<3,==2.4", [">=2.0", "<3", "==2.4"]),
+ ],
+ )
+ def test_iteration(self, spec, expected_items):
+ spec = SpecifierSet(spec)
+ items = {str(item) for item in spec}
+ assert items == set(expected_items)
+
+
+class TestLegacySpecifier:
+ def test_legacy_specifier_is_deprecated(self):
+ with warnings.catch_warnings(record=True) as w:
+ LegacySpecifier(">=some-legacy-version")
+ assert len(w) == 1
+ assert issubclass(w[0].category, DeprecationWarning)
+
+ @pytest.mark.parametrize(
+ ("version", "spec", "expected"),
+ [
+ (v, s, True)
+ for v, s in [
+ # Test the equality operation
+ ("2.0", "==2"),
+ ("2.0", "==2.0"),
+ ("2.0", "==2.0.0"),
+ # 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"),
+ # 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"),
+ # Test the less than operation
+ ("1", "<2"),
+ ("2.0", "<2.1"),
+ ]
+ ]
+ + [
+ (v, s, False)
+ for v, s in [
+ # Test the equality operation
+ ("2.1", "==2"),
+ ("2.1", "==2.0"),
+ ("2.1", "==2.0.0"),
+ # Test the in-equality operation
+ ("2.0", "!=2"),
+ ("2.0", "!=2.0"),
+ ("2.0", "!=2.0.0"),
+ # 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"),
+ # Test the less than operation
+ ("3", "<2"),
+ ]
+ ],
+ )
+ def test_specifiers(self, version, spec, expected):
+ spec = LegacySpecifier(spec, prereleases=True)
+
+ if expected:
+ # Test that the plain string form works
+ assert version in spec
+ assert spec.contains(version)
+
+ # Test that the version instance form works
+ assert LegacyVersion(version) in spec
+ assert spec.contains(LegacyVersion(version))
+ else:
+ # Test that the plain string form works
+ assert version not in spec
+ assert not spec.contains(version)
+
+ # Test that the version instance form works
+ assert LegacyVersion(version) not in spec
+ assert not spec.contains(LegacyVersion(version))
+
+ def test_specifier_explicit_prereleases(self):
+ spec = LegacySpecifier(">=1.0")
+ assert not spec.prereleases
+ spec.prereleases = True
+ assert spec.prereleases
+
+ spec = LegacySpecifier(">=1.0", prereleases=False)
+ assert not spec.prereleases
+ spec.prereleases = True
+ assert spec.prereleases
+
+ spec = LegacySpecifier(">=1.0", prereleases=True)
+ assert spec.prereleases
+ spec.prereleases = False
+ assert not spec.prereleases
+
+ spec = LegacySpecifier(">=1.0", prereleases=True)
+ assert spec.prereleases
+ spec.prereleases = None
+ assert not spec.prereleases
+
+
+class TestSpecifierSet:
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_empty_specifier(self, version):
+ spec = SpecifierSet(prereleases=True)
+
+ assert version in spec
+ assert spec.contains(version)
+ assert parse(version) in spec
+ assert spec.contains(parse(version))
+
+ def test_specifier_prereleases_explicit(self):
+ spec = SpecifierSet()
+ assert not spec.prereleases
+ assert "1.0.dev1" not in spec
+ assert not spec.contains("1.0.dev1")
+ spec.prereleases = True
+ assert spec.prereleases
+ assert "1.0.dev1" in spec
+ assert spec.contains("1.0.dev1")
+
+ spec = SpecifierSet(prereleases=True)
+ assert spec.prereleases
+ assert "1.0.dev1" in spec
+ assert spec.contains("1.0.dev1")
+ spec.prereleases = False
+ assert not spec.prereleases
+ assert "1.0.dev1" not in spec
+ assert not spec.contains("1.0.dev1")
+
+ spec = SpecifierSet(prereleases=True)
+ assert spec.prereleases
+ assert "1.0.dev1" in spec
+ assert spec.contains("1.0.dev1")
+ spec.prereleases = None
+ assert not spec.prereleases
+ assert "1.0.dev1" not in spec
+ assert not spec.contains("1.0.dev1")
+
+ def test_specifier_contains_prereleases(self):
+ spec = SpecifierSet()
+ assert spec.prereleases is None
+ assert not spec.contains("1.0.dev1")
+ assert spec.contains("1.0.dev1", prereleases=True)
+
+ spec = SpecifierSet(prereleases=True)
+ assert spec.prereleases
+ assert spec.contains("1.0.dev1")
+ assert not spec.contains("1.0.dev1", prereleases=False)
+
+ @pytest.mark.parametrize(
+ ("specifier", "specifier_prereleases", "prereleases", "input", "expected"),
+ [
+ # General test of the filter method
+ ("", None, None, ["1.0", "2.0a1"], ["1.0"]),
+ (">=1.0.dev1", None, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ ("", None, None, ["1.0a1"], ["1.0a1"]),
+ ("", None, None, ["1.0", Version("2.0")], ["1.0", Version("2.0")]),
+ ("", None, None, ["2.0dog", "1.0"], ["1.0"]),
+ # Test overriding with the prereleases parameter on filter
+ ("", None, False, ["1.0a1"], []),
+ (">=1.0.dev1", None, False, ["1.0", "2.0a1"], ["1.0"]),
+ ("", None, True, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ # Test overriding with the overall specifier
+ ("", True, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ ("", False, None, ["1.0", "2.0a1"], ["1.0"]),
+ (">=1.0.dev1", True, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ (">=1.0.dev1", False, None, ["1.0", "2.0a1"], ["1.0"]),
+ ("", True, None, ["1.0a1"], ["1.0a1"]),
+ ("", False, None, ["1.0a1"], []),
+ ],
+ )
+ def test_specifier_filter(
+ self, specifier_prereleases, specifier, prereleases, input, expected
+ ):
+ if specifier_prereleases is None:
+ spec = SpecifierSet(specifier)
+ else:
+ spec = SpecifierSet(specifier, prereleases=specifier_prereleases)
+
+ kwargs = {"prereleases": prereleases} if prereleases is not None else {}
+
+ assert list(spec.filter(input, **kwargs)) == expected
+
+ def test_legacy_specifiers_combined(self):
+ spec = SpecifierSet("<3,>1-1-1")
+ assert "2.0" in spec
+
+ @pytest.mark.parametrize(
+ ("specifier", "expected"),
+ [
+ # Single item specifiers should just be reflexive
+ ("!=2.0", "!=2.0"),
+ ("<2.0", "<2.0"),
+ ("<=2.0", "<=2.0"),
+ ("==2.0", "==2.0"),
+ (">2.0", ">2.0"),
+ (">=2.0", ">=2.0"),
+ ("~=2.0", "~=2.0"),
+ # Spaces should be removed
+ ("< 2", "<2"),
+ # Multiple item specifiers should work
+ ("!=2.0,>1.0", "!=2.0,>1.0"),
+ ("!=2.0 ,>1.0", "!=2.0,>1.0"),
+ ],
+ )
+ def test_specifiers_str_and_repr(self, specifier, expected):
+ spec = SpecifierSet(specifier)
+
+ assert str(spec) == expected
+ assert repr(spec) == f"<SpecifierSet({expected!r})>"
+
+ @pytest.mark.parametrize("specifier", SPECIFIERS + LEGACY_SPECIFIERS)
+ def test_specifiers_hash(self, specifier):
+ assert hash(SpecifierSet(specifier)) == hash(SpecifierSet(specifier))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "expected"), [(">2.0", "<5.0", ">2.0,<5.0")]
+ )
+ def test_specifiers_combine(self, left, right, expected):
+ result = SpecifierSet(left) & SpecifierSet(right)
+ assert result == SpecifierSet(expected)
+
+ result = SpecifierSet(left) & right
+ assert result == SpecifierSet(expected)
+
+ result = SpecifierSet(left, prereleases=True) & SpecifierSet(right)
+ assert result == SpecifierSet(expected)
+ assert result.prereleases
+
+ result = SpecifierSet(left, prereleases=False) & SpecifierSet(right)
+ assert result == SpecifierSet(expected)
+ assert not result.prereleases
+
+ result = SpecifierSet(left) & SpecifierSet(right, prereleases=True)
+ assert result == SpecifierSet(expected)
+ assert result.prereleases
+
+ result = SpecifierSet(left) & SpecifierSet(right, prereleases=False)
+ assert result == SpecifierSet(expected)
+ assert not result.prereleases
+
+ result = SpecifierSet(left, prereleases=True) & SpecifierSet(
+ right, prereleases=True
+ )
+ assert result == SpecifierSet(expected)
+ assert result.prereleases
+
+ result = SpecifierSet(left, prereleases=False) & SpecifierSet(
+ right, prereleases=False
+ )
+ assert result == SpecifierSet(expected)
+ assert not result.prereleases
+
+ with pytest.raises(ValueError):
+ result = SpecifierSet(left, prereleases=True) & SpecifierSet(
+ right, prereleases=False
+ )
+
+ with pytest.raises(ValueError):
+ result = SpecifierSet(left, prereleases=False) & SpecifierSet(
+ right, prereleases=True
+ )
+
+ def test_specifiers_combine_not_implemented(self):
+ with pytest.raises(TypeError):
+ SpecifierSet() & 12
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.ne) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(SpecifierSet(left), SpecifierSet(right))
+ assert op(SpecifierSet(left), Specifier(right))
+ assert op(Specifier(left), SpecifierSet(right))
+ assert op(left, SpecifierSet(right))
+ assert op(SpecifierSet(left), right)
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.ne) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.eq) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(SpecifierSet(left), SpecifierSet(right))
+ assert not op(SpecifierSet(left), Specifier(right))
+ assert not op(Specifier(left), SpecifierSet(right))
+ assert not op(left, SpecifierSet(right))
+ assert not op(SpecifierSet(left), right)
+
+ @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")])
+ def test_comparison_canonicalizes(self, left, right):
+ assert SpecifierSet(left) == SpecifierSet(right)
+ assert left == SpecifierSet(right)
+ assert SpecifierSet(left) == right
+
+ def test_comparison_non_specifier(self):
+ assert SpecifierSet("==1.0") != 12
+ assert not SpecifierSet("==1.0") == 12
+
+ @pytest.mark.parametrize(
+ ("version", "specifier", "expected"),
+ [
+ ("1.0.0+local", "==1.0.0", True),
+ ("1.0.0+local", "!=1.0.0", False),
+ ("1.0.0+local", "<=1.0.0", True),
+ ("1.0.0+local", ">=1.0.0", True),
+ ("1.0.0+local", "<1.0.0", False),
+ ("1.0.0+local", ">1.0.0", False),
+ ],
+ )
+ def test_comparison_ignores_local(self, version, specifier, expected):
+ assert (Version(version) in SpecifierSet(specifier)) == expected
diff --git a/tests/test_structures.py b/tests/test_structures.py
new file mode 100644
index 0000000..f8115e5
--- /dev/null
+++ b/tests/test_structures.py
@@ -0,0 +1,59 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import pytest
+
+from packaging._structures import Infinity, NegativeInfinity
+
+
+def test_infinity_repr():
+ repr(Infinity) == "Infinity"
+
+
+def test_negative_infinity_repr():
+ repr(NegativeInfinity) == "-Infinity"
+
+
+def test_infinity_hash():
+ assert hash(Infinity) == hash(Infinity)
+
+
+def test_negative_infinity_hash():
+ assert hash(NegativeInfinity) == hash(NegativeInfinity)
+
+
+@pytest.mark.parametrize("left", [1, "a", ("b", 4)])
+def test_infinity_comparison(left):
+ assert left < Infinity
+ assert left <= Infinity
+ assert not left == Infinity
+ assert left != Infinity
+ assert not left > Infinity
+ assert not left >= Infinity
+
+
+@pytest.mark.parametrize("left", [1, "a", ("b", 4)])
+def test_negative_infinity_lesser(left):
+ assert not left < NegativeInfinity
+ assert not left <= NegativeInfinity
+ assert not left == NegativeInfinity
+ assert left != NegativeInfinity
+ assert left > NegativeInfinity
+ assert left >= NegativeInfinity
+
+
+def test_infinity_equal():
+ assert Infinity == Infinity
+
+
+def test_negative_infinity_equal():
+ assert NegativeInfinity == NegativeInfinity
+
+
+def test_negate_infinity():
+ assert isinstance(-Infinity, NegativeInfinity.__class__)
+
+
+def test_negate_negative_infinity():
+ assert isinstance(-NegativeInfinity, Infinity.__class__)
diff --git a/tests/test_tags.py b/tests/test_tags.py
new file mode 100644
index 0000000..446dee4
--- /dev/null
+++ b/tests/test_tags.py
@@ -0,0 +1,1191 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+
+import collections.abc
+
+try:
+ import ctypes
+except ImportError:
+ ctypes = None
+import os
+import pathlib
+import platform
+import sys
+import sysconfig
+import types
+
+import pretend
+import pytest
+
+from packaging import tags
+from packaging._musllinux import _MuslVersion
+
+
+@pytest.fixture
+def example_tag():
+ return tags.Tag("py3", "none", "any")
+
+
+@pytest.fixture
+def manylinux_module(monkeypatch):
+ monkeypatch.setattr(tags._manylinux, "_get_glibc_version", lambda *args: (2, 20))
+ module_name = "_manylinux"
+ module = types.ModuleType(module_name)
+ monkeypatch.setitem(sys.modules, module_name, module)
+ return module
+
+
+@pytest.fixture
+def mock_interpreter_name(monkeypatch):
+ def mock(name):
+ name = name.lower()
+ if sys.implementation.name != name:
+ monkeypatch.setattr(sys.implementation, "name", name)
+ return True
+ return False
+
+ return mock
+
+
+class TestTag:
+ def test_lowercasing(self):
+ tag = tags.Tag("PY3", "None", "ANY")
+ assert tag.interpreter == "py3"
+ assert tag.abi == "none"
+ assert tag.platform == "any"
+
+ def test_equality(self):
+ args = "py3", "none", "any"
+ assert tags.Tag(*args) == tags.Tag(*args)
+
+ def test_equality_fails_with_non_tag(self):
+ assert not tags.Tag("py3", "none", "any") == "non-tag"
+
+ def test_hashing(self, example_tag):
+ tags = {example_tag} # Should not raise TypeError.
+ assert example_tag in tags
+
+ def test_hash_equality(self, example_tag):
+ equal_tag = tags.Tag("py3", "none", "any")
+ assert example_tag == equal_tag # Sanity check.
+ assert example_tag.__hash__() == equal_tag.__hash__()
+
+ def test_str(self, example_tag):
+ assert str(example_tag) == "py3-none-any"
+
+ def test_repr(self, example_tag):
+ assert repr(example_tag) == "<py3-none-any @ {tag_id}>".format(
+ tag_id=id(example_tag)
+ )
+
+ def test_attribute_access(self, example_tag):
+ assert example_tag.interpreter == "py3"
+ assert example_tag.abi == "none"
+ assert example_tag.platform == "any"
+
+
+class TestParseTag:
+ def test_simple(self, example_tag):
+ parsed_tags = tags.parse_tag(str(example_tag))
+ assert parsed_tags == {example_tag}
+
+ def test_multi_interpreter(self, example_tag):
+ expected = {example_tag, tags.Tag("py2", "none", "any")}
+ given = tags.parse_tag("py2.py3-none-any")
+ assert given == expected
+
+ def test_multi_platform(self):
+ expected = {
+ tags.Tag("cp37", "cp37m", platform)
+ for platform in (
+ "macosx_10_6_intel",
+ "macosx_10_9_intel",
+ "macosx_10_9_x86_64",
+ "macosx_10_10_intel",
+ "macosx_10_10_x86_64",
+ )
+ }
+ given = tags.parse_tag(
+ "cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64."
+ "macosx_10_10_intel.macosx_10_10_x86_64"
+ )
+ assert given == expected
+
+
+class TestInterpreterName:
+ def test_sys_implementation_name(self, monkeypatch):
+ class MockImplementation:
+ pass
+
+ mock_implementation = MockImplementation()
+ mock_implementation.name = "sillywalk"
+ monkeypatch.setattr(sys, "implementation", mock_implementation, raising=False)
+ assert tags.interpreter_name() == "sillywalk"
+
+ def test_interpreter_short_names(self, mock_interpreter_name, monkeypatch):
+ mock_interpreter_name("cpython")
+ assert tags.interpreter_name() == "cp"
+
+
+class TestInterpreterVersion:
+ def test_warn(self, monkeypatch):
+ class MockConfigVar:
+ def __init__(self, return_):
+ self.warn = None
+ self._return = return_
+
+ def __call__(self, name, warn):
+ self.warn = warn
+ return self._return
+
+ mock_config_var = MockConfigVar("38")
+ monkeypatch.setattr(tags, "_get_config_var", mock_config_var)
+ tags.interpreter_version(warn=True)
+ assert mock_config_var.warn
+
+ def test_python_version_nodot(self, monkeypatch):
+ monkeypatch.setattr(tags, "_get_config_var", lambda var, warn: "NN")
+ assert tags.interpreter_version() == "NN"
+
+ @pytest.mark.parametrize(
+ "version_info,version_str",
+ [
+ ((1, 2, 3), "12"),
+ ((1, 12, 3), "112"),
+ ((11, 2, 3), "112"),
+ ((11, 12, 3), "1112"),
+ ((1, 2, 13), "12"),
+ ],
+ )
+ def test_sys_version_info(self, version_info, version_str, monkeypatch):
+ monkeypatch.setattr(tags, "_get_config_var", lambda *args, **kwargs: None)
+ monkeypatch.setattr(sys, "version_info", version_info)
+ assert tags.interpreter_version() == version_str
+
+
+class TestMacOSPlatforms:
+ @pytest.mark.parametrize(
+ "arch, is_32bit, expected",
+ [
+ ("i386", True, "i386"),
+ ("ppc", True, "ppc"),
+ ("x86_64", False, "x86_64"),
+ ("x86_64", True, "i386"),
+ ("ppc64", False, "ppc64"),
+ ("ppc64", True, "ppc"),
+ ],
+ )
+ def test_architectures(self, arch, is_32bit, expected):
+ assert tags._mac_arch(arch, is_32bit=is_32bit) == expected
+
+ @pytest.mark.parametrize(
+ "version,arch,expected",
+ [
+ (
+ (10, 15),
+ "x86_64",
+ ["x86_64", "intel", "fat64", "fat32", "universal2", "universal"],
+ ),
+ (
+ (10, 4),
+ "x86_64",
+ ["x86_64", "intel", "fat64", "fat32", "universal2", "universal"],
+ ),
+ ((10, 3), "x86_64", []),
+ ((10, 15), "i386", ["i386", "intel", "fat32", "fat", "universal"]),
+ ((10, 4), "i386", ["i386", "intel", "fat32", "fat", "universal"]),
+ ((10, 3), "intel", ["intel", "universal"]),
+ ((10, 5), "intel", ["intel", "universal"]),
+ ((10, 15), "intel", ["intel", "universal"]),
+ ((10, 3), "i386", []),
+ ((10, 15), "ppc64", []),
+ ((10, 6), "ppc64", []),
+ ((10, 5), "ppc64", ["ppc64", "fat64", "universal"]),
+ ((10, 3), "ppc64", []),
+ ((10, 15), "ppc", []),
+ ((10, 7), "ppc", []),
+ ((10, 6), "ppc", ["ppc", "fat32", "fat", "universal"]),
+ ((10, 0), "ppc", ["ppc", "fat32", "fat", "universal"]),
+ ((11, 0), "riscv", ["riscv"]),
+ (
+ (11, 0),
+ "x86_64",
+ ["x86_64", "intel", "fat64", "fat32", "universal2", "universal"],
+ ),
+ ((11, 0), "arm64", ["arm64", "universal2"]),
+ ((11, 1), "arm64", ["arm64", "universal2"]),
+ ((12, 0), "arm64", ["arm64", "universal2"]),
+ ],
+ )
+ def test_binary_formats(self, version, arch, expected):
+ assert tags._mac_binary_formats(version, arch) == expected
+
+ def test_version_detection(self, monkeypatch):
+ if platform.system() != "Darwin":
+ monkeypatch.setattr(
+ platform, "mac_ver", lambda: ("10.14", ("", "", ""), "x86_64")
+ )
+ version = platform.mac_ver()[0].split(".")
+ major = version[0]
+ minor = version[1] if major == "10" else "0"
+ expected = f"macosx_{major}_{minor}"
+
+ platforms = list(tags.mac_platforms(arch="x86_64"))
+ print(platforms, expected)
+ assert platforms[0].startswith(expected)
+
+ @pytest.mark.parametrize("arch", ["x86_64", "i386"])
+ def test_arch_detection(self, arch, monkeypatch):
+ if platform.system() != "Darwin" or platform.mac_ver()[2] != arch:
+ monkeypatch.setattr(
+ platform, "mac_ver", lambda: ("10.14", ("", "", ""), arch)
+ )
+ monkeypatch.setattr(tags, "_mac_arch", lambda *args: arch)
+ assert next(tags.mac_platforms((10, 14))).endswith(arch)
+
+ def test_mac_platforms(self):
+ platforms = list(tags.mac_platforms((10, 5), "x86_64"))
+ assert platforms == [
+ "macosx_10_5_x86_64",
+ "macosx_10_5_intel",
+ "macosx_10_5_fat64",
+ "macosx_10_5_fat32",
+ "macosx_10_5_universal2",
+ "macosx_10_5_universal",
+ "macosx_10_4_x86_64",
+ "macosx_10_4_intel",
+ "macosx_10_4_fat64",
+ "macosx_10_4_fat32",
+ "macosx_10_4_universal2",
+ "macosx_10_4_universal",
+ ]
+
+ assert len(list(tags.mac_platforms((10, 17), "x86_64"))) == 14 * 6
+
+ assert not list(tags.mac_platforms((10, 0), "x86_64"))
+
+ @pytest.mark.parametrize("major,minor", [(11, 0), (11, 3), (12, 0), (12, 3)])
+ def test_macos_11(self, major, minor):
+ platforms = list(tags.mac_platforms((major, minor), "x86_64"))
+ assert "macosx_11_0_arm64" not in platforms
+ assert "macosx_11_0_x86_64" in platforms
+ assert "macosx_11_3_x86_64" not in platforms
+ assert "macosx_11_0_universal" in platforms
+ assert "macosx_11_0_universal2" in platforms
+ # Mac OS "10.16" is the version number that binaries compiled against an old
+ # (pre 11.0) SDK will see. It can also be enabled explicitly for a process
+ # with the environment variable SYSTEM_VERSION_COMPAT=1.
+ assert "macosx_10_16_x86_64" in platforms
+ assert "macosx_10_15_x86_64" in platforms
+ assert "macosx_10_15_universal2" in platforms
+ assert "macosx_10_4_x86_64" in platforms
+ assert "macosx_10_3_x86_64" not in platforms
+ if major >= 12:
+ assert "macosx_12_0_x86_64" in platforms
+ assert "macosx_12_0_universal" in platforms
+ assert "macosx_12_0_universal2" in platforms
+
+ platforms = list(tags.mac_platforms((major, minor), "arm64"))
+ assert "macosx_11_0_arm64" in platforms
+ assert "macosx_11_3_arm64" not in platforms
+ assert "macosx_11_0_universal" not in platforms
+ assert "macosx_11_0_universal2" in platforms
+ assert "macosx_10_15_universal2" in platforms
+ assert "macosx_10_15_x86_64" not in platforms
+ assert "macosx_10_4_x86_64" not in platforms
+ assert "macosx_10_3_x86_64" not in platforms
+ if major >= 12:
+ assert "macosx_12_0_arm64" in platforms
+ assert "macosx_12_0_universal2" in platforms
+
+
+class TestManylinuxPlatform:
+ def teardown_method(self):
+ # Clear the version cache
+ tags._manylinux._get_glibc_version.cache_clear()
+
+ def test_get_config_var_does_not_log(self, monkeypatch):
+ debug = pretend.call_recorder(lambda *a: None)
+ monkeypatch.setattr(tags.logger, "debug", debug)
+ tags._get_config_var("missing")
+ assert debug.calls == []
+
+ def test_get_config_var_does_log(self, monkeypatch):
+ debug = pretend.call_recorder(lambda *a: None)
+ monkeypatch.setattr(tags.logger, "debug", debug)
+ tags._get_config_var("missing", warn=True)
+ assert debug.calls == [
+ pretend.call(
+ "Config variable '%s' is unset, Python ABI tag may be incorrect",
+ "missing",
+ )
+ ]
+
+ @pytest.mark.parametrize(
+ "arch,is_32bit,expected",
+ [
+ ("linux-x86_64", False, "linux_x86_64"),
+ ("linux-x86_64", True, "linux_i686"),
+ ("linux-aarch64", False, "linux_aarch64"),
+ ("linux-aarch64", True, "linux_armv7l"),
+ ],
+ )
+ def test_linux_platforms_32_64bit_on_64bit_os(
+ self, arch, is_32bit, expected, monkeypatch
+ ):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: arch)
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda *args: False)
+ linux_platform = list(tags._linux_platforms(is_32bit=is_32bit))[-1]
+ assert linux_platform == expected
+
+ def test_linux_platforms_manylinux_unsupported(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda *args: False)
+ linux_platform = list(tags._linux_platforms(is_32bit=False))
+ assert linux_platform == ["linux_x86_64"]
+
+ def test_linux_platforms_manylinux1(self, monkeypatch):
+ monkeypatch.setattr(
+ tags._manylinux, "_is_compatible", lambda name, *args: name == "manylinux1"
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(platform, "machine", lambda: "x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ arch = platform.machine()
+ assert platforms == ["manylinux1_" + arch, "linux_" + arch]
+
+ def test_linux_platforms_manylinux2010(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(platform, "machine", lambda: "x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.12", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ arch = platform.machine()
+ expected = [
+ "manylinux_2_12_" + arch,
+ "manylinux2010_" + arch,
+ "manylinux_2_11_" + arch,
+ "manylinux_2_10_" + arch,
+ "manylinux_2_9_" + arch,
+ "manylinux_2_8_" + arch,
+ "manylinux_2_7_" + arch,
+ "manylinux_2_6_" + arch,
+ "manylinux_2_5_" + arch,
+ "manylinux1_" + arch,
+ "linux_" + arch,
+ ]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux2014(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(platform, "machine", lambda: "x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.17", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ arch = platform.machine()
+ expected = [
+ "manylinux_2_17_" + arch,
+ "manylinux2014_" + arch,
+ "manylinux_2_16_" + arch,
+ "manylinux_2_15_" + arch,
+ "manylinux_2_14_" + arch,
+ "manylinux_2_13_" + arch,
+ "manylinux_2_12_" + arch,
+ "manylinux2010_" + arch,
+ "manylinux_2_11_" + arch,
+ "manylinux_2_10_" + arch,
+ "manylinux_2_9_" + arch,
+ "manylinux_2_8_" + arch,
+ "manylinux_2_7_" + arch,
+ "manylinux_2_6_" + arch,
+ "manylinux_2_5_" + arch,
+ "manylinux1_" + arch,
+ "linux_" + arch,
+ ]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux2014_armhf_abi(self, monkeypatch):
+ monkeypatch.setattr(tags._manylinux, "_glibc_version_string", lambda: "2.30")
+ monkeypatch.setattr(
+ tags._manylinux,
+ "_is_compatible",
+ lambda name, *args: name == "manylinux2014",
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_armv7l")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ "hello-world-armv7l-armhf",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = ["manylinux2014_armv7l", "linux_armv7l"]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux2014_i386_abi(self, monkeypatch):
+ monkeypatch.setattr(tags._manylinux, "_glibc_version_string", lambda: "2.17")
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ "hello-world-x86_64-i386",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = [
+ "manylinux_2_17_i686",
+ "manylinux2014_i686",
+ "manylinux_2_16_i686",
+ "manylinux_2_15_i686",
+ "manylinux_2_14_i686",
+ "manylinux_2_13_i686",
+ "manylinux_2_12_i686",
+ "manylinux2010_i686",
+ "manylinux_2_11_i686",
+ "manylinux_2_10_i686",
+ "manylinux_2_9_i686",
+ "manylinux_2_8_i686",
+ "manylinux_2_7_i686",
+ "manylinux_2_6_i686",
+ "manylinux_2_5_i686",
+ "manylinux1_i686",
+ "linux_i686",
+ ]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux_glibc3(self, monkeypatch):
+ # test for a future glic 3.x version
+ monkeypatch.setattr(tags._manylinux, "_glibc_version_string", lambda: "3.2")
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda name, *args: True)
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_aarch64")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ "hello-world-aarch64",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ expected = (
+ ["manylinux_3_2_aarch64", "manylinux_3_1_aarch64", "manylinux_3_0_aarch64"]
+ + [f"manylinux_2_{i}_aarch64" for i in range(50, 16, -1)]
+ + ["manylinux2014_aarch64", "linux_aarch64"]
+ )
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "native_arch, cross32_arch, musl_version",
+ [
+ ("aarch64", "armv7l", _MuslVersion(1, 1)),
+ ("i386", "i386", _MuslVersion(1, 2)),
+ ("x86_64", "i686", _MuslVersion(1, 2)),
+ ],
+ )
+ @pytest.mark.parametrize("cross32", [True, False], ids=["cross", "native"])
+ def test_linux_platforms_musllinux(
+ self, monkeypatch, native_arch, cross32_arch, musl_version, cross32
+ ):
+ fake_executable = str(
+ pathlib.Path(__file__)
+ .parent.joinpath("musllinux", f"musl-{native_arch}")
+ .resolve()
+ )
+ monkeypatch.setattr(tags._musllinux.sys, "executable", fake_executable)
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{native_arch}")
+ monkeypatch.setattr(tags._manylinux, "platform_tags", lambda *_: ())
+
+ recorder = pretend.call_recorder(lambda _: musl_version)
+ monkeypatch.setattr(tags._musllinux, "_get_musl_version", recorder)
+
+ platforms = list(tags._linux_platforms(is_32bit=cross32))
+ target_arch = cross32_arch if cross32 else native_arch
+ expected = [
+ f"musllinux_{musl_version[0]}_{minor}_{target_arch}"
+ for minor in range(musl_version[1], -1, -1)
+ ] + [f"linux_{target_arch}"]
+ assert platforms == expected
+
+ assert recorder.calls == [pretend.call(fake_executable)]
+
+ def test_linux_platforms_manylinux2014_armv6l(self, monkeypatch):
+ monkeypatch.setattr(
+ tags._manylinux, "_is_compatible", lambda name, _: name == "manylinux2014"
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_armv6l")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = ["linux_armv6l"]
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "machine, abi, alt_machine",
+ [("x86_64", "x32", "i686"), ("armv7l", "armel", "armv7l")],
+ )
+ def test_linux_platforms_not_manylinux_abi(
+ self, monkeypatch, machine, abi, alt_machine
+ ):
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda name, _: False)
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{machine}")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{machine}-{abi}",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = [f"linux_{alt_machine}"]
+ assert platforms == expected
+
+
+@pytest.mark.parametrize(
+ "platform_name,dispatch_func",
+ [
+ ("Darwin", "mac_platforms"),
+ ("Linux", "_linux_platforms"),
+ ("Generic", "_generic_platforms"),
+ ],
+)
+def test_platform_tags(platform_name, dispatch_func, monkeypatch):
+ expected = ["sillywalk"]
+ monkeypatch.setattr(platform, "system", lambda: platform_name)
+ monkeypatch.setattr(tags, dispatch_func, lambda: expected)
+ assert tags.platform_tags() == expected
+
+
+class TestCPythonABI:
+ @pytest.mark.parametrize(
+ "py_debug,gettotalrefcount,result",
+ [(1, False, True), (0, False, False), (None, True, True)],
+ )
+ def test_debug(self, py_debug, gettotalrefcount, result, monkeypatch):
+ config = {"Py_DEBUG": py_debug, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": 2}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ if gettotalrefcount:
+ monkeypatch.setattr(sys, "gettotalrefcount", 1, raising=False)
+ expected = ["cp37d" if result else "cp37"]
+ assert tags._cpython_abis((3, 7)) == expected
+
+ def test_debug_file_extension(self, monkeypatch):
+ config = {"Py_DEBUG": None}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ monkeypatch.delattr(sys, "gettotalrefcount", raising=False)
+ monkeypatch.setattr(tags, "EXTENSION_SUFFIXES", {"_d.pyd"})
+ assert tags._cpython_abis((3, 8)) == ["cp38d", "cp38"]
+
+ @pytest.mark.parametrize(
+ "debug,expected", [(True, ["cp38d", "cp38"]), (False, ["cp38"])]
+ )
+ def test__debug_cp38(self, debug, expected, monkeypatch):
+ config = {"Py_DEBUG": debug}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ assert tags._cpython_abis((3, 8)) == expected
+
+ @pytest.mark.parametrize(
+ "pymalloc,version,result",
+ [
+ (1, (3, 7), True),
+ (0, (3, 7), False),
+ (None, (3, 7), True),
+ (1, (3, 8), False),
+ ],
+ )
+ def test_pymalloc(self, pymalloc, version, result, monkeypatch):
+ config = {"Py_DEBUG": 0, "WITH_PYMALLOC": pymalloc, "Py_UNICODE_SIZE": 2}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ base_abi = f"cp{version[0]}{version[1]}"
+ expected = [base_abi + "m" if result else base_abi]
+ assert tags._cpython_abis(version) == expected
+
+ @pytest.mark.parametrize(
+ "unicode_size,maxunicode,version,result",
+ [
+ (4, 0x10FFFF, (3, 2), True),
+ (2, 0xFFFF, (3, 2), False),
+ (None, 0x10FFFF, (3, 2), True),
+ (None, 0xFFFF, (3, 2), False),
+ (4, 0x10FFFF, (3, 3), False),
+ ],
+ )
+ def test_wide_unicode(self, unicode_size, maxunicode, version, result, monkeypatch):
+ config = {"Py_DEBUG": 0, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": unicode_size}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ monkeypatch.setattr(sys, "maxunicode", maxunicode)
+ base_abi = "cp" + tags._version_nodot(version)
+ expected = [base_abi + "u" if result else base_abi]
+ assert tags._cpython_abis(version) == expected
+
+
+class TestCPythonTags:
+ def test_iterator_returned(self):
+ result_iterator = tags.cpython_tags(
+ (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"]
+ )
+ assert isinstance(result_iterator, collections.abc.Iterator)
+
+ def test_all_args(self):
+ result_iterator = tags.cpython_tags(
+ (3, 11), ["cp311d", "cp311"], ["plat1", "plat2"]
+ )
+ result = list(result_iterator)
+ assert result == [
+ tags.Tag("cp311", "cp311d", "plat1"),
+ tags.Tag("cp311", "cp311d", "plat2"),
+ tags.Tag("cp311", "cp311", "plat1"),
+ tags.Tag("cp311", "cp311", "plat2"),
+ tags.Tag("cp311", "abi3", "plat1"),
+ tags.Tag("cp311", "abi3", "plat2"),
+ tags.Tag("cp311", "none", "plat1"),
+ tags.Tag("cp311", "none", "plat2"),
+ tags.Tag("cp310", "abi3", "plat1"),
+ tags.Tag("cp310", "abi3", "plat2"),
+ tags.Tag("cp39", "abi3", "plat1"),
+ tags.Tag("cp39", "abi3", "plat2"),
+ tags.Tag("cp38", "abi3", "plat1"),
+ tags.Tag("cp38", "abi3", "plat2"),
+ tags.Tag("cp37", "abi3", "plat1"),
+ tags.Tag("cp37", "abi3", "plat2"),
+ tags.Tag("cp36", "abi3", "plat1"),
+ tags.Tag("cp36", "abi3", "plat2"),
+ tags.Tag("cp35", "abi3", "plat1"),
+ tags.Tag("cp35", "abi3", "plat2"),
+ tags.Tag("cp34", "abi3", "plat1"),
+ tags.Tag("cp34", "abi3", "plat2"),
+ tags.Tag("cp33", "abi3", "plat1"),
+ tags.Tag("cp33", "abi3", "plat2"),
+ tags.Tag("cp32", "abi3", "plat1"),
+ tags.Tag("cp32", "abi3", "plat2"),
+ ]
+ result_iterator = tags.cpython_tags(
+ (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"]
+ )
+ result = list(result_iterator)
+ assert result == [
+ tags.Tag("cp38", "cp38d", "plat1"),
+ tags.Tag("cp38", "cp38d", "plat2"),
+ tags.Tag("cp38", "cp38", "plat1"),
+ tags.Tag("cp38", "cp38", "plat2"),
+ tags.Tag("cp38", "abi3", "plat1"),
+ tags.Tag("cp38", "abi3", "plat2"),
+ tags.Tag("cp38", "none", "plat1"),
+ tags.Tag("cp38", "none", "plat2"),
+ tags.Tag("cp37", "abi3", "plat1"),
+ tags.Tag("cp37", "abi3", "plat2"),
+ tags.Tag("cp36", "abi3", "plat1"),
+ tags.Tag("cp36", "abi3", "plat2"),
+ tags.Tag("cp35", "abi3", "plat1"),
+ tags.Tag("cp35", "abi3", "plat2"),
+ tags.Tag("cp34", "abi3", "plat1"),
+ tags.Tag("cp34", "abi3", "plat2"),
+ tags.Tag("cp33", "abi3", "plat1"),
+ tags.Tag("cp33", "abi3", "plat2"),
+ tags.Tag("cp32", "abi3", "plat1"),
+ tags.Tag("cp32", "abi3", "plat2"),
+ ]
+
+ result = list(tags.cpython_tags((3, 3), ["cp33m"], ["plat1", "plat2"]))
+ assert result == [
+ tags.Tag("cp33", "cp33m", "plat1"),
+ tags.Tag("cp33", "cp33m", "plat2"),
+ tags.Tag("cp33", "abi3", "plat1"),
+ tags.Tag("cp33", "abi3", "plat2"),
+ tags.Tag("cp33", "none", "plat1"),
+ tags.Tag("cp33", "none", "plat2"),
+ tags.Tag("cp32", "abi3", "plat1"),
+ tags.Tag("cp32", "abi3", "plat2"),
+ ]
+
+ def test_python_version_defaults(self):
+ tag = next(tags.cpython_tags(abis=["abi3"], platforms=["any"]))
+ interpreter = "cp" + tags._version_nodot(sys.version_info[:2])
+ assert interpreter == tag.interpreter
+
+ def test_abi_defaults(self, monkeypatch):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp38"])
+ result = list(tags.cpython_tags((3, 8), platforms=["any"]))
+ assert tags.Tag("cp38", "cp38", "any") in result
+ assert tags.Tag("cp38", "abi3", "any") in result
+ assert tags.Tag("cp38", "none", "any") in result
+
+ def test_abi_defaults_needs_underscore(self, monkeypatch):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp311"])
+ result = list(tags.cpython_tags((3, 11), platforms=["any"]))
+ assert tags.Tag("cp311", "cp311", "any") in result
+ assert tags.Tag("cp311", "abi3", "any") in result
+ assert tags.Tag("cp311", "none", "any") in result
+
+ def test_platforms_defaults(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: ["plat1"])
+ result = list(tags.cpython_tags((3, 8), abis=["whatever"]))
+ assert tags.Tag("cp38", "whatever", "plat1") in result
+
+ def test_platforms_defaults_needs_underscore(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: ["plat1"])
+ result = list(tags.cpython_tags((3, 11), abis=["whatever"]))
+ assert tags.Tag("cp311", "whatever", "plat1") in result
+
+ def test_major_only_python_version(self):
+ result = list(tags.cpython_tags((3,), ["abi"], ["plat"]))
+ assert result == [
+ tags.Tag("cp3", "abi", "plat"),
+ tags.Tag("cp3", "none", "plat"),
+ ]
+
+ def test_major_only_python_version_with_default_abis(self):
+ result = list(tags.cpython_tags((3,), platforms=["plat"]))
+ assert result == [tags.Tag("cp3", "none", "plat")]
+
+ @pytest.mark.parametrize("abis", [[], ["abi3"], ["none"]])
+ def test_skip_redundant_abis(self, abis):
+ results = list(tags.cpython_tags((3, 0), abis=abis, platforms=["any"]))
+ assert results == [tags.Tag("cp30", "none", "any")]
+
+ def test_abi3_python33(self):
+ results = list(tags.cpython_tags((3, 3), abis=["cp33"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp33", "cp33", "plat"),
+ tags.Tag("cp33", "abi3", "plat"),
+ tags.Tag("cp33", "none", "plat"),
+ tags.Tag("cp32", "abi3", "plat"),
+ ]
+
+ def test_no_excess_abi3_python32(self):
+ results = list(tags.cpython_tags((3, 2), abis=["cp32"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp32", "cp32", "plat"),
+ tags.Tag("cp32", "abi3", "plat"),
+ tags.Tag("cp32", "none", "plat"),
+ ]
+
+ def test_no_abi3_python31(self):
+ results = list(tags.cpython_tags((3, 1), abis=["cp31"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp31", "cp31", "plat"),
+ tags.Tag("cp31", "none", "plat"),
+ ]
+
+ def test_no_abi3_python27(self):
+ results = list(tags.cpython_tags((2, 7), abis=["cp27"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp27", "cp27", "plat"),
+ tags.Tag("cp27", "none", "plat"),
+ ]
+
+
+class TestGenericTags:
+ @pytest.mark.skipif(
+ not sysconfig.get_config_var("SOABI"), reason="SOABI not defined"
+ )
+ def test__generic_abi_soabi_provided(self):
+ abi = sysconfig.get_config_var("SOABI").replace(".", "_").replace("-", "_")
+ assert [abi] == list(tags._generic_abi())
+
+ def test__generic_abi(self, monkeypatch):
+ monkeypatch.setattr(
+ sysconfig, "get_config_var", lambda key: "cpython-37m-darwin"
+ )
+ assert list(tags._generic_abi()) == ["cpython_37m_darwin"]
+
+ def test__generic_abi_no_soabi(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_config_var", lambda key: None)
+ assert not list(tags._generic_abi())
+
+ def test_generic_platforms(self):
+ platform = sysconfig.get_platform().replace("-", "_")
+ platform = platform.replace(".", "_")
+ assert list(tags._generic_platforms()) == [platform]
+
+ def test_iterator_returned(self):
+ result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"])
+ assert isinstance(result_iterator, collections.abc.Iterator)
+
+ def test_all_args(self):
+ result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"])
+ result = list(result_iterator)
+ assert result == [
+ tags.Tag("sillywalk33", "abi", "plat1"),
+ tags.Tag("sillywalk33", "abi", "plat2"),
+ tags.Tag("sillywalk33", "none", "plat1"),
+ tags.Tag("sillywalk33", "none", "plat2"),
+ ]
+
+ @pytest.mark.parametrize("abi", [[], ["none"]])
+ def test_abi_unspecified(self, abi):
+ no_abi = list(tags.generic_tags("sillywalk34", abi, ["plat1", "plat2"]))
+ assert no_abi == [
+ tags.Tag("sillywalk34", "none", "plat1"),
+ tags.Tag("sillywalk34", "none", "plat2"),
+ ]
+
+ def test_interpreter_default(self, monkeypatch):
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "sillywalk")
+ monkeypatch.setattr(tags, "interpreter_version", lambda warn: "NN")
+ result = list(tags.generic_tags(abis=["none"], platforms=["any"]))
+ assert result == [tags.Tag("sillywalkNN", "none", "any")]
+
+ def test_abis_default(self, monkeypatch):
+ monkeypatch.setattr(tags, "_generic_abi", lambda: iter(["abi"]))
+ result = list(tags.generic_tags(interpreter="sillywalk", platforms=["any"]))
+ assert result == [
+ tags.Tag("sillywalk", "abi", "any"),
+ tags.Tag("sillywalk", "none", "any"),
+ ]
+
+ def test_platforms_default(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: ["plat"])
+ result = list(tags.generic_tags(interpreter="sillywalk", abis=["none"]))
+ assert result == [tags.Tag("sillywalk", "none", "plat")]
+
+
+class TestCompatibleTags:
+ def test_all_args(self):
+ result = list(tags.compatible_tags((3, 3), "cp33", ["plat1", "plat2"]))
+ assert result == [
+ tags.Tag("py33", "none", "plat1"),
+ tags.Tag("py33", "none", "plat2"),
+ tags.Tag("py3", "none", "plat1"),
+ tags.Tag("py3", "none", "plat2"),
+ tags.Tag("py32", "none", "plat1"),
+ tags.Tag("py32", "none", "plat2"),
+ tags.Tag("py31", "none", "plat1"),
+ tags.Tag("py31", "none", "plat2"),
+ tags.Tag("py30", "none", "plat1"),
+ tags.Tag("py30", "none", "plat2"),
+ tags.Tag("cp33", "none", "any"),
+ tags.Tag("py33", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py32", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_all_args_needs_underscore(self):
+ result = list(tags.compatible_tags((3, 11), "cp311", ["plat1", "plat2"]))
+ assert result == [
+ tags.Tag("py311", "none", "plat1"),
+ tags.Tag("py311", "none", "plat2"),
+ tags.Tag("py3", "none", "plat1"),
+ tags.Tag("py3", "none", "plat2"),
+ tags.Tag("py310", "none", "plat1"),
+ tags.Tag("py310", "none", "plat2"),
+ tags.Tag("py39", "none", "plat1"),
+ tags.Tag("py39", "none", "plat2"),
+ tags.Tag("py38", "none", "plat1"),
+ tags.Tag("py38", "none", "plat2"),
+ tags.Tag("py37", "none", "plat1"),
+ tags.Tag("py37", "none", "plat2"),
+ tags.Tag("py36", "none", "plat1"),
+ tags.Tag("py36", "none", "plat2"),
+ tags.Tag("py35", "none", "plat1"),
+ tags.Tag("py35", "none", "plat2"),
+ tags.Tag("py34", "none", "plat1"),
+ tags.Tag("py34", "none", "plat2"),
+ tags.Tag("py33", "none", "plat1"),
+ tags.Tag("py33", "none", "plat2"),
+ tags.Tag("py32", "none", "plat1"),
+ tags.Tag("py32", "none", "plat2"),
+ tags.Tag("py31", "none", "plat1"),
+ tags.Tag("py31", "none", "plat2"),
+ tags.Tag("py30", "none", "plat1"),
+ tags.Tag("py30", "none", "plat2"),
+ tags.Tag("cp311", "none", "any"),
+ tags.Tag("py311", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py310", "none", "any"),
+ tags.Tag("py39", "none", "any"),
+ tags.Tag("py38", "none", "any"),
+ tags.Tag("py37", "none", "any"),
+ tags.Tag("py36", "none", "any"),
+ tags.Tag("py35", "none", "any"),
+ tags.Tag("py34", "none", "any"),
+ tags.Tag("py33", "none", "any"),
+ tags.Tag("py32", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_major_only_python_version(self):
+ result = list(tags.compatible_tags((3,), "cp33", ["plat"]))
+ assert result == [
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("cp33", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ ]
+
+ def test_default_python_version(self, monkeypatch):
+ monkeypatch.setattr(sys, "version_info", (3, 1))
+ result = list(tags.compatible_tags(interpreter="cp31", platforms=["plat"]))
+ assert result == [
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("cp31", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_default_python_version_needs_underscore(self, monkeypatch):
+ monkeypatch.setattr(sys, "version_info", (3, 11))
+ result = list(tags.compatible_tags(interpreter="cp311", platforms=["plat"]))
+ assert result == [
+ tags.Tag("py311", "none", "plat"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py310", "none", "plat"),
+ tags.Tag("py39", "none", "plat"),
+ tags.Tag("py38", "none", "plat"),
+ tags.Tag("py37", "none", "plat"),
+ tags.Tag("py36", "none", "plat"),
+ tags.Tag("py35", "none", "plat"),
+ tags.Tag("py34", "none", "plat"),
+ tags.Tag("py33", "none", "plat"),
+ tags.Tag("py32", "none", "plat"),
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("cp311", "none", "any"),
+ tags.Tag("py311", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py310", "none", "any"),
+ tags.Tag("py39", "none", "any"),
+ tags.Tag("py38", "none", "any"),
+ tags.Tag("py37", "none", "any"),
+ tags.Tag("py36", "none", "any"),
+ tags.Tag("py35", "none", "any"),
+ tags.Tag("py34", "none", "any"),
+ tags.Tag("py33", "none", "any"),
+ tags.Tag("py32", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_default_interpreter(self):
+ result = list(tags.compatible_tags((3, 1), platforms=["plat"]))
+ assert result == [
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_default_platforms(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: iter(["plat", "plat2"]))
+ result = list(tags.compatible_tags((3, 1), "cp31"))
+ assert result == [
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py31", "none", "plat2"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py3", "none", "plat2"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("py30", "none", "plat2"),
+ tags.Tag("cp31", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+
+class TestSysTags:
+ def teardown_method(self):
+ # Clear the version cache
+ tags._glibc_version = []
+
+ @pytest.mark.parametrize(
+ "name,expected",
+ [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")],
+ )
+ def test_interpreter_name(self, name, expected, mock_interpreter_name):
+ mock_interpreter_name(name)
+ assert tags.interpreter_name() == expected
+
+ def test_iterator(self):
+ assert isinstance(tags.sys_tags(), collections.abc.Iterator)
+
+ def test_mac_cpython(self, mock_interpreter_name, monkeypatch):
+ if mock_interpreter_name("CPython"):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"])
+ if platform.system() != "Darwin":
+ monkeypatch.setattr(platform, "system", lambda: "Darwin")
+ monkeypatch.setattr(tags, "mac_platforms", lambda: ["macosx_10_5_x86_64"])
+ abis = tags._cpython_abis(sys.version_info[:2])
+ platforms = list(tags.mac_platforms())
+ result = list(tags.sys_tags())
+ assert len(abis) == 1
+ assert result[0] == tags.Tag(
+ "cp" + tags._version_nodot(sys.version_info[:2]), abis[0], platforms[0]
+ )
+ assert result[-1] == tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+
+ def test_windows_cpython(self, mock_interpreter_name, monkeypatch):
+ if mock_interpreter_name("CPython"):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"])
+ if platform.system() != "Windows":
+ monkeypatch.setattr(platform, "system", lambda: "Windows")
+ monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"])
+ abis = list(tags._cpython_abis(sys.version_info[:2]))
+ platforms = list(tags._generic_platforms())
+ result = list(tags.sys_tags())
+ interpreter = "cp" + tags._version_nodot(sys.version_info[:2])
+ assert len(abis) == 1
+ expected = tags.Tag(interpreter, abis[0], platforms[0])
+ assert result[0] == expected
+ expected = tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+ assert result[-1] == expected
+
+ def test_linux_cpython(self, mock_interpreter_name, monkeypatch):
+ if mock_interpreter_name("CPython"):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"])
+ if platform.system() != "Linux":
+ monkeypatch.setattr(platform, "system", lambda: "Linux")
+ monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"])
+ abis = list(tags._cpython_abis(sys.version_info[:2]))
+ platforms = list(tags._linux_platforms())
+ result = list(tags.sys_tags())
+ expected_interpreter = "cp" + tags._version_nodot(sys.version_info[:2])
+ assert len(abis) == 1
+ assert result[0] == tags.Tag(expected_interpreter, abis[0], platforms[0])
+ expected = tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+ assert result[-1] == expected
+
+ def test_generic(self, monkeypatch):
+ monkeypatch.setattr(platform, "system", lambda: "Generic")
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "generic")
+
+ result = list(tags.sys_tags())
+ expected = tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+ assert result[-1] == expected
+
+ def test_linux_platforms_manylinux2014_armv6l(self, monkeypatch, manylinux_module):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_armv6l")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = ["linux_armv6l"]
+ assert platforms == expected
+
+ def test_skip_manylinux_2014(self, monkeypatch, manylinux_module):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_ppc64")
+ monkeypatch.setattr(tags._manylinux, "_get_glibc_version", lambda: (2, 20))
+ monkeypatch.setattr(
+ manylinux_module, "manylinux2014_compatible", False, raising=False
+ )
+ expected = [
+ "manylinux_2_20_ppc64",
+ "manylinux_2_19_ppc64",
+ "manylinux_2_18_ppc64",
+ # "manylinux2014_ppc64", # this one is skipped
+ # "manylinux_2_17_ppc64", # this one is also skipped
+ "linux_ppc64",
+ ]
+ platforms = list(tags._linux_platforms())
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "machine, abi, alt_machine",
+ [("x86_64", "x32", "i686"), ("armv7l", "armel", "armv7l")],
+ )
+ def test_linux_platforms_not_manylinux_abi(
+ self, monkeypatch, manylinux_module, machine, abi, alt_machine
+ ):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{machine}")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{machine}-{abi}",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = [f"linux_{alt_machine}"]
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "machine, major, minor, tf", [("x86_64", 2, 20, False), ("s390x", 2, 22, True)]
+ )
+ def test_linux_use_manylinux_compatible(
+ self, monkeypatch, manylinux_module, machine, major, minor, tf
+ ):
+ def manylinux_compatible(tag_major, tag_minor, tag_arch):
+ if tag_major == 2 and tag_minor == 22:
+ return tag_arch == "s390x"
+ return False
+
+ monkeypatch.setattr(
+ tags._manylinux,
+ "_get_glibc_version",
+ lambda: (major, minor),
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{machine}")
+ monkeypatch.setattr(
+ manylinux_module,
+ "manylinux_compatible",
+ manylinux_compatible,
+ raising=False,
+ )
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ if tf:
+ expected = [f"manylinux_2_22_{machine}"]
+ else:
+ expected = []
+ expected.append(f"linux_{machine}")
+ assert platforms == expected
+
+ def test_linux_use_manylinux_compatible_none(self, monkeypatch, manylinux_module):
+ def manylinux_compatible(tag_major, tag_minor, tag_arch):
+ if tag_major == 2 and tag_minor < 25:
+ return False
+ return None
+
+ monkeypatch.setattr(tags._manylinux, "_get_glibc_version", lambda: (2, 30))
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(
+ manylinux_module,
+ "manylinux_compatible",
+ manylinux_compatible,
+ raising=False,
+ )
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ expected = [
+ "manylinux_2_30_x86_64",
+ "manylinux_2_29_x86_64",
+ "manylinux_2_28_x86_64",
+ "manylinux_2_27_x86_64",
+ "manylinux_2_26_x86_64",
+ "manylinux_2_25_x86_64",
+ "linux_x86_64",
+ ]
+ assert platforms == expected
+
+ def test_pypy_first_none_any_tag(self, monkeypatch):
+ # When building the complete list of pypy tags, make sure the first
+ # <interpreter>-none-any one is pp3-none-any
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")
+
+ for tag in tags.sys_tags():
+ if tag.abi == "none" and tag.platform == "any":
+ break
+
+ assert tag == tags.Tag("pp3", "none", "any")
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..be52d67
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,124 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import pytest
+
+from packaging.tags import Tag
+from packaging.utils import (
+ InvalidSdistFilename,
+ InvalidWheelFilename,
+ canonicalize_name,
+ canonicalize_version,
+ parse_sdist_filename,
+ parse_wheel_filename,
+)
+from packaging.version import Version
+
+
+@pytest.mark.parametrize(
+ ("name", "expected"),
+ [
+ ("foo", "foo"),
+ ("Foo", "foo"),
+ ("fOo", "foo"),
+ ("foo.bar", "foo-bar"),
+ ("Foo.Bar", "foo-bar"),
+ ("Foo.....Bar", "foo-bar"),
+ ("foo_bar", "foo-bar"),
+ ("foo___bar", "foo-bar"),
+ ("foo-bar", "foo-bar"),
+ ("foo----bar", "foo-bar"),
+ ],
+)
+def test_canonicalize_name(name, expected):
+ assert canonicalize_name(name) == expected
+
+
+@pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ (Version("1.4.0"), "1.4"),
+ ("1.4.0", "1.4"),
+ ("1.40.0", "1.40"),
+ ("1.4.0.0.00.000.0000", "1.4"),
+ ("1.0", "1"),
+ ("1.0+abc", "1+abc"),
+ ("1.0.dev0", "1.dev0"),
+ ("1.0.post0", "1.post0"),
+ ("1.0a0", "1a0"),
+ ("1.0rc0", "1rc0"),
+ ("100!0.0", "100!0"),
+ ("1.0.1-test7", "1.0.1-test7"), # LegacyVersion is unchanged
+ ],
+)
+def test_canonicalize_version(version, expected):
+ assert canonicalize_version(version) == expected
+
+
+@pytest.mark.parametrize(
+ ("filename", "name", "version", "build", "tags"),
+ [
+ (
+ "foo-1.0-py3-none-any.whl",
+ "foo",
+ Version("1.0"),
+ (),
+ {Tag("py3", "none", "any")},
+ ),
+ (
+ "some_PACKAGE-1.0-py3-none-any.whl",
+ "some-package",
+ Version("1.0"),
+ (),
+ {Tag("py3", "none", "any")},
+ ),
+ (
+ "foo-1.0-1000-py3-none-any.whl",
+ "foo",
+ Version("1.0"),
+ (1000, ""),
+ {Tag("py3", "none", "any")},
+ ),
+ (
+ "foo-1.0-1000abc-py3-none-any.whl",
+ "foo",
+ Version("1.0"),
+ (1000, "abc"),
+ {Tag("py3", "none", "any")},
+ ),
+ ],
+)
+def test_parse_wheel_filename(filename, name, version, build, tags):
+ assert parse_wheel_filename(filename) == (name, version, build, tags)
+
+
+@pytest.mark.parametrize(
+ ("filename"),
+ [
+ ("foo-1.0.whl"), # Missing tags
+ ("foo-1.0-py3-none-any.wheel"), # Incorrect file extension (`.wheel`)
+ ("foo__bar-1.0-py3-none-any.whl"), # Invalid name (`__`)
+ ("foo#bar-1.0-py3-none-any.whl"), # Invalid name (`#`)
+ # Build number doesn't start with a digit (`abc`)
+ ("foo-1.0-abc-py3-none-any.whl"),
+ ("foo-1.0-200-py3-none-any-junk.whl"), # Too many dashes (`-junk`)
+ ],
+)
+def test_parse_wheel_invalid_filename(filename):
+ with pytest.raises(InvalidWheelFilename):
+ parse_wheel_filename(filename)
+
+
+@pytest.mark.parametrize(
+ ("filename", "name", "version"),
+ [("foo-1.0.tar.gz", "foo", Version("1.0")), ("foo-1.0.zip", "foo", Version("1.0"))],
+)
+def test_parse_sdist_filename(filename, name, version):
+ assert parse_sdist_filename(filename) == (name, version)
+
+
+@pytest.mark.parametrize(("filename"), [("foo-1.0.xz"), ("foo1.0.tar.gz")])
+def test_parse_sdist_invalid_filename(filename):
+ with pytest.raises(InvalidSdistFilename):
+ parse_sdist_filename(filename)
diff --git a/tests/test_version.py b/tests/test_version.py
new file mode 100644
index 0000000..5f2251e
--- /dev/null
+++ b/tests/test_version.py
@@ -0,0 +1,904 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import itertools
+import operator
+import warnings
+
+import pretend
+import pytest
+
+from packaging.version import InvalidVersion, LegacyVersion, Version, parse
+
+
+@pytest.mark.parametrize(
+ ("version", "klass"), [("1.0", Version), ("1-1-1", LegacyVersion)]
+)
+def test_parse(version, klass):
+ assert isinstance(parse(version), klass)
+
+
+# This list must be in the correct sorting order
+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",
+]
+
+
+class TestVersion:
+ @pytest.mark.parametrize("version", VERSIONS)
+ def test_valid_versions(self, version):
+ Version(version)
+
+ @pytest.mark.parametrize(
+ "version",
+ [
+ # Non sensical 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",
+ ],
+ )
+ def test_invalid_versions(self, version):
+ with pytest.raises(InvalidVersion):
+ Version(version)
+
+ @pytest.mark.parametrize(
+ ("version", "normalized"),
+ [
+ # 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"),
+ ],
+ )
+ def test_normalized_versions(self, version, normalized):
+ assert str(Version(version)) == normalized
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("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"),
+ ],
+ )
+ def test_version_str_repr(self, version, expected):
+ assert str(Version(version)) == expected
+ assert repr(Version(version)) == f"<Version({expected!r})>"
+
+ def test_version_rc_and_c_equals(self):
+ assert Version("1.0rc1") == Version("1.0c1")
+
+ @pytest.mark.parametrize("version", VERSIONS)
+ def test_version_hash(self, version):
+ assert hash(Version(version)) == hash(Version(version))
+
+ @pytest.mark.parametrize(
+ ("version", "public"),
+ [
+ ("1.0", "1.0"),
+ ("1.0.dev0", "1.0.dev0"),
+ ("1.0.dev6", "1.0.dev6"),
+ ("1.0a1", "1.0a1"),
+ ("1.0a1.post5", "1.0a1.post5"),
+ ("1.0a1.post5.dev6", "1.0a1.post5.dev6"),
+ ("1.0rc4", "1.0rc4"),
+ ("1.0.post5", "1.0.post5"),
+ ("1!1.0", "1!1.0"),
+ ("1!1.0.dev6", "1!1.0.dev6"),
+ ("1!1.0a1", "1!1.0a1"),
+ ("1!1.0a1.post5", "1!1.0a1.post5"),
+ ("1!1.0a1.post5.dev6", "1!1.0a1.post5.dev6"),
+ ("1!1.0rc4", "1!1.0rc4"),
+ ("1!1.0.post5", "1!1.0.post5"),
+ ("1.0+deadbeef", "1.0"),
+ ("1.0.dev6+deadbeef", "1.0.dev6"),
+ ("1.0a1+deadbeef", "1.0a1"),
+ ("1.0a1.post5+deadbeef", "1.0a1.post5"),
+ ("1.0a1.post5.dev6+deadbeef", "1.0a1.post5.dev6"),
+ ("1.0rc4+deadbeef", "1.0rc4"),
+ ("1.0.post5+deadbeef", "1.0.post5"),
+ ("1!1.0+deadbeef", "1!1.0"),
+ ("1!1.0.dev6+deadbeef", "1!1.0.dev6"),
+ ("1!1.0a1+deadbeef", "1!1.0a1"),
+ ("1!1.0a1.post5+deadbeef", "1!1.0a1.post5"),
+ ("1!1.0a1.post5.dev6+deadbeef", "1!1.0a1.post5.dev6"),
+ ("1!1.0rc4+deadbeef", "1!1.0rc4"),
+ ("1!1.0.post5+deadbeef", "1!1.0.post5"),
+ ],
+ )
+ def test_version_public(self, version, public):
+ assert Version(version).public == public
+
+ @pytest.mark.parametrize(
+ ("version", "base_version"),
+ [
+ ("1.0", "1.0"),
+ ("1.0.dev0", "1.0"),
+ ("1.0.dev6", "1.0"),
+ ("1.0a1", "1.0"),
+ ("1.0a1.post5", "1.0"),
+ ("1.0a1.post5.dev6", "1.0"),
+ ("1.0rc4", "1.0"),
+ ("1.0.post5", "1.0"),
+ ("1!1.0", "1!1.0"),
+ ("1!1.0.dev6", "1!1.0"),
+ ("1!1.0a1", "1!1.0"),
+ ("1!1.0a1.post5", "1!1.0"),
+ ("1!1.0a1.post5.dev6", "1!1.0"),
+ ("1!1.0rc4", "1!1.0"),
+ ("1!1.0.post5", "1!1.0"),
+ ("1.0+deadbeef", "1.0"),
+ ("1.0.dev6+deadbeef", "1.0"),
+ ("1.0a1+deadbeef", "1.0"),
+ ("1.0a1.post5+deadbeef", "1.0"),
+ ("1.0a1.post5.dev6+deadbeef", "1.0"),
+ ("1.0rc4+deadbeef", "1.0"),
+ ("1.0.post5+deadbeef", "1.0"),
+ ("1!1.0+deadbeef", "1!1.0"),
+ ("1!1.0.dev6+deadbeef", "1!1.0"),
+ ("1!1.0a1+deadbeef", "1!1.0"),
+ ("1!1.0a1.post5+deadbeef", "1!1.0"),
+ ("1!1.0a1.post5.dev6+deadbeef", "1!1.0"),
+ ("1!1.0rc4+deadbeef", "1!1.0"),
+ ("1!1.0.post5+deadbeef", "1!1.0"),
+ ],
+ )
+ def test_version_base_version(self, version, base_version):
+ assert Version(version).base_version == base_version
+
+ @pytest.mark.parametrize(
+ ("version", "epoch"),
+ [
+ ("1.0", 0),
+ ("1.0.dev0", 0),
+ ("1.0.dev6", 0),
+ ("1.0a1", 0),
+ ("1.0a1.post5", 0),
+ ("1.0a1.post5.dev6", 0),
+ ("1.0rc4", 0),
+ ("1.0.post5", 0),
+ ("1!1.0", 1),
+ ("1!1.0.dev6", 1),
+ ("1!1.0a1", 1),
+ ("1!1.0a1.post5", 1),
+ ("1!1.0a1.post5.dev6", 1),
+ ("1!1.0rc4", 1),
+ ("1!1.0.post5", 1),
+ ("1.0+deadbeef", 0),
+ ("1.0.dev6+deadbeef", 0),
+ ("1.0a1+deadbeef", 0),
+ ("1.0a1.post5+deadbeef", 0),
+ ("1.0a1.post5.dev6+deadbeef", 0),
+ ("1.0rc4+deadbeef", 0),
+ ("1.0.post5+deadbeef", 0),
+ ("1!1.0+deadbeef", 1),
+ ("1!1.0.dev6+deadbeef", 1),
+ ("1!1.0a1+deadbeef", 1),
+ ("1!1.0a1.post5+deadbeef", 1),
+ ("1!1.0a1.post5.dev6+deadbeef", 1),
+ ("1!1.0rc4+deadbeef", 1),
+ ("1!1.0.post5+deadbeef", 1),
+ ],
+ )
+ def test_version_epoch(self, version, epoch):
+ assert Version(version).epoch == epoch
+
+ @pytest.mark.parametrize(
+ ("version", "release"),
+ [
+ ("1.0", (1, 0)),
+ ("1.0.dev0", (1, 0)),
+ ("1.0.dev6", (1, 0)),
+ ("1.0a1", (1, 0)),
+ ("1.0a1.post5", (1, 0)),
+ ("1.0a1.post5.dev6", (1, 0)),
+ ("1.0rc4", (1, 0)),
+ ("1.0.post5", (1, 0)),
+ ("1!1.0", (1, 0)),
+ ("1!1.0.dev6", (1, 0)),
+ ("1!1.0a1", (1, 0)),
+ ("1!1.0a1.post5", (1, 0)),
+ ("1!1.0a1.post5.dev6", (1, 0)),
+ ("1!1.0rc4", (1, 0)),
+ ("1!1.0.post5", (1, 0)),
+ ("1.0+deadbeef", (1, 0)),
+ ("1.0.dev6+deadbeef", (1, 0)),
+ ("1.0a1+deadbeef", (1, 0)),
+ ("1.0a1.post5+deadbeef", (1, 0)),
+ ("1.0a1.post5.dev6+deadbeef", (1, 0)),
+ ("1.0rc4+deadbeef", (1, 0)),
+ ("1.0.post5+deadbeef", (1, 0)),
+ ("1!1.0+deadbeef", (1, 0)),
+ ("1!1.0.dev6+deadbeef", (1, 0)),
+ ("1!1.0a1+deadbeef", (1, 0)),
+ ("1!1.0a1.post5+deadbeef", (1, 0)),
+ ("1!1.0a1.post5.dev6+deadbeef", (1, 0)),
+ ("1!1.0rc4+deadbeef", (1, 0)),
+ ("1!1.0.post5+deadbeef", (1, 0)),
+ ],
+ )
+ def test_version_release(self, version, release):
+ assert Version(version).release == release
+
+ @pytest.mark.parametrize(
+ ("version", "local"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", None),
+ ("1.0.dev6", None),
+ ("1.0a1", None),
+ ("1.0a1.post5", None),
+ ("1.0a1.post5.dev6", None),
+ ("1.0rc4", None),
+ ("1.0.post5", None),
+ ("1!1.0", None),
+ ("1!1.0.dev6", None),
+ ("1!1.0a1", None),
+ ("1!1.0a1.post5", None),
+ ("1!1.0a1.post5.dev6", None),
+ ("1!1.0rc4", None),
+ ("1!1.0.post5", None),
+ ("1.0+deadbeef", "deadbeef"),
+ ("1.0.dev6+deadbeef", "deadbeef"),
+ ("1.0a1+deadbeef", "deadbeef"),
+ ("1.0a1.post5+deadbeef", "deadbeef"),
+ ("1.0a1.post5.dev6+deadbeef", "deadbeef"),
+ ("1.0rc4+deadbeef", "deadbeef"),
+ ("1.0.post5+deadbeef", "deadbeef"),
+ ("1!1.0+deadbeef", "deadbeef"),
+ ("1!1.0.dev6+deadbeef", "deadbeef"),
+ ("1!1.0a1+deadbeef", "deadbeef"),
+ ("1!1.0a1.post5+deadbeef", "deadbeef"),
+ ("1!1.0a1.post5.dev6+deadbeef", "deadbeef"),
+ ("1!1.0rc4+deadbeef", "deadbeef"),
+ ("1!1.0.post5+deadbeef", "deadbeef"),
+ ],
+ )
+ def test_version_local(self, version, local):
+ assert Version(version).local == local
+
+ @pytest.mark.parametrize(
+ ("version", "pre"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", None),
+ ("1.0.dev6", None),
+ ("1.0a1", ("a", 1)),
+ ("1.0a1.post5", ("a", 1)),
+ ("1.0a1.post5.dev6", ("a", 1)),
+ ("1.0rc4", ("rc", 4)),
+ ("1.0.post5", None),
+ ("1!1.0", None),
+ ("1!1.0.dev6", None),
+ ("1!1.0a1", ("a", 1)),
+ ("1!1.0a1.post5", ("a", 1)),
+ ("1!1.0a1.post5.dev6", ("a", 1)),
+ ("1!1.0rc4", ("rc", 4)),
+ ("1!1.0.post5", None),
+ ("1.0+deadbeef", None),
+ ("1.0.dev6+deadbeef", None),
+ ("1.0a1+deadbeef", ("a", 1)),
+ ("1.0a1.post5+deadbeef", ("a", 1)),
+ ("1.0a1.post5.dev6+deadbeef", ("a", 1)),
+ ("1.0rc4+deadbeef", ("rc", 4)),
+ ("1.0.post5+deadbeef", None),
+ ("1!1.0+deadbeef", None),
+ ("1!1.0.dev6+deadbeef", None),
+ ("1!1.0a1+deadbeef", ("a", 1)),
+ ("1!1.0a1.post5+deadbeef", ("a", 1)),
+ ("1!1.0a1.post5.dev6+deadbeef", ("a", 1)),
+ ("1!1.0rc4+deadbeef", ("rc", 4)),
+ ("1!1.0.post5+deadbeef", None),
+ ],
+ )
+ def test_version_pre(self, version, pre):
+ assert Version(version).pre == pre
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("1.0.dev0", True),
+ ("1.0.dev1", True),
+ ("1.0a1.dev1", True),
+ ("1.0b1.dev1", True),
+ ("1.0c1.dev1", True),
+ ("1.0rc1.dev1", True),
+ ("1.0a1", True),
+ ("1.0b1", True),
+ ("1.0c1", True),
+ ("1.0rc1", True),
+ ("1.0a1.post1.dev1", True),
+ ("1.0b1.post1.dev1", True),
+ ("1.0c1.post1.dev1", True),
+ ("1.0rc1.post1.dev1", True),
+ ("1.0a1.post1", True),
+ ("1.0b1.post1", True),
+ ("1.0c1.post1", True),
+ ("1.0rc1.post1", True),
+ ("1.0", False),
+ ("1.0+dev", False),
+ ("1.0.post1", False),
+ ("1.0.post1+dev", False),
+ ],
+ )
+ def test_version_is_prerelease(self, version, expected):
+ assert Version(version).is_prerelease is expected
+
+ @pytest.mark.parametrize(
+ ("version", "dev"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", 0),
+ ("1.0.dev6", 6),
+ ("1.0a1", None),
+ ("1.0a1.post5", None),
+ ("1.0a1.post5.dev6", 6),
+ ("1.0rc4", None),
+ ("1.0.post5", None),
+ ("1!1.0", None),
+ ("1!1.0.dev6", 6),
+ ("1!1.0a1", None),
+ ("1!1.0a1.post5", None),
+ ("1!1.0a1.post5.dev6", 6),
+ ("1!1.0rc4", None),
+ ("1!1.0.post5", None),
+ ("1.0+deadbeef", None),
+ ("1.0.dev6+deadbeef", 6),
+ ("1.0a1+deadbeef", None),
+ ("1.0a1.post5+deadbeef", None),
+ ("1.0a1.post5.dev6+deadbeef", 6),
+ ("1.0rc4+deadbeef", None),
+ ("1.0.post5+deadbeef", None),
+ ("1!1.0+deadbeef", None),
+ ("1!1.0.dev6+deadbeef", 6),
+ ("1!1.0a1+deadbeef", None),
+ ("1!1.0a1.post5+deadbeef", None),
+ ("1!1.0a1.post5.dev6+deadbeef", 6),
+ ("1!1.0rc4+deadbeef", None),
+ ("1!1.0.post5+deadbeef", None),
+ ],
+ )
+ def test_version_dev(self, version, dev):
+ assert Version(version).dev == dev
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("1.0", False),
+ ("1.0.dev0", True),
+ ("1.0.dev6", True),
+ ("1.0a1", False),
+ ("1.0a1.post5", False),
+ ("1.0a1.post5.dev6", True),
+ ("1.0rc4", False),
+ ("1.0.post5", False),
+ ("1!1.0", False),
+ ("1!1.0.dev6", True),
+ ("1!1.0a1", False),
+ ("1!1.0a1.post5", False),
+ ("1!1.0a1.post5.dev6", True),
+ ("1!1.0rc4", False),
+ ("1!1.0.post5", False),
+ ("1.0+deadbeef", False),
+ ("1.0.dev6+deadbeef", True),
+ ("1.0a1+deadbeef", False),
+ ("1.0a1.post5+deadbeef", False),
+ ("1.0a1.post5.dev6+deadbeef", True),
+ ("1.0rc4+deadbeef", False),
+ ("1.0.post5+deadbeef", False),
+ ("1!1.0+deadbeef", False),
+ ("1!1.0.dev6+deadbeef", True),
+ ("1!1.0a1+deadbeef", False),
+ ("1!1.0a1.post5+deadbeef", False),
+ ("1!1.0a1.post5.dev6+deadbeef", True),
+ ("1!1.0rc4+deadbeef", False),
+ ("1!1.0.post5+deadbeef", False),
+ ],
+ )
+ def test_version_is_devrelease(self, version, expected):
+ assert Version(version).is_devrelease is expected
+
+ @pytest.mark.parametrize(
+ ("version", "post"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", None),
+ ("1.0.dev6", None),
+ ("1.0a1", None),
+ ("1.0a1.post5", 5),
+ ("1.0a1.post5.dev6", 5),
+ ("1.0rc4", None),
+ ("1.0.post5", 5),
+ ("1!1.0", None),
+ ("1!1.0.dev6", None),
+ ("1!1.0a1", None),
+ ("1!1.0a1.post5", 5),
+ ("1!1.0a1.post5.dev6", 5),
+ ("1!1.0rc4", None),
+ ("1!1.0.post5", 5),
+ ("1.0+deadbeef", None),
+ ("1.0.dev6+deadbeef", None),
+ ("1.0a1+deadbeef", None),
+ ("1.0a1.post5+deadbeef", 5),
+ ("1.0a1.post5.dev6+deadbeef", 5),
+ ("1.0rc4+deadbeef", None),
+ ("1.0.post5+deadbeef", 5),
+ ("1!1.0+deadbeef", None),
+ ("1!1.0.dev6+deadbeef", None),
+ ("1!1.0a1+deadbeef", None),
+ ("1!1.0a1.post5+deadbeef", 5),
+ ("1!1.0a1.post5.dev6+deadbeef", 5),
+ ("1!1.0rc4+deadbeef", None),
+ ("1!1.0.post5+deadbeef", 5),
+ ],
+ )
+ def test_version_post(self, version, post):
+ assert Version(version).post == post
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("1.0.dev1", False),
+ ("1.0", False),
+ ("1.0+foo", False),
+ ("1.0.post1.dev1", True),
+ ("1.0.post1", True),
+ ],
+ )
+ def test_version_is_postrelease(self, version, expected):
+ assert Version(version).is_postrelease is expected
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of VERSIONS that
+ # should be True for the given operator
+ itertools.chain(
+ *
+ # Verify that the less than (<) operator works correctly
+ [
+ [(x, y, operator.lt) for y in VERSIONS[i + 1 :]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the less than equal (<=) operator works correctly
+ [
+ [(x, y, operator.le) for y in VERSIONS[i:]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in VERSIONS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.ne) for j, y in enumerate(VERSIONS) if i != j]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the greater than equal (>=) operator works correctly
+ [
+ [(x, y, operator.ge) for y in VERSIONS[: i + 1]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the greater than (>) operator works correctly
+ [
+ [(x, y, operator.gt) for y in VERSIONS[:i]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(Version(left), Version(right))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of VERSIONS that
+ # should be False for the given operator
+ itertools.chain(
+ *
+ # Verify that the less than (<) operator works correctly
+ [
+ [(x, y, operator.lt) for y in VERSIONS[: i + 1]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the less than equal (<=) operator works correctly
+ [
+ [(x, y, operator.le) for y in VERSIONS[:i]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the equal (==) operator works correctly
+ [
+ [(x, y, operator.eq) for j, y in enumerate(VERSIONS) if i != j]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [[(x, x, operator.ne) for x in VERSIONS]]
+ +
+ # Verify that the greater than equal (>=) operator works correctly
+ [
+ [(x, y, operator.ge) for y in VERSIONS[i + 1 :]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the greater than (>) operator works correctly
+ [
+ [(x, y, operator.gt) for y in VERSIONS[i:]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(Version(left), Version(right))
+
+ @pytest.mark.parametrize("op", ["lt", "le", "eq", "ge", "gt", "ne"])
+ def test_dunder_op_returns_notimplemented(self, op):
+ method = getattr(Version, f"__{op}__")
+ assert method(Version("1"), 1) is NotImplemented
+
+ @pytest.mark.parametrize(("op", "expected"), [("eq", False), ("ne", True)])
+ def test_compare_other(self, op, expected):
+ other = pretend.stub(**{f"__{op}__": lambda other: NotImplemented})
+
+ assert getattr(operator, op)(Version("1"), other) is expected
+
+ def test_compare_legacyversion_version(self):
+ result = sorted([Version("0"), LegacyVersion("1")])
+ assert result == [LegacyVersion("1"), Version("0")]
+
+ def test_major_version(self):
+ assert Version("2.1.0").major == 2
+
+ def test_minor_version(self):
+ assert Version("2.1.0").minor == 1
+ assert Version("2").minor == 0
+
+ def test_micro_version(self):
+ assert Version("2.1.3").micro == 3
+ assert Version("2.1").micro == 0
+ assert Version("2").micro == 0
+
+
+LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1-0", "2.0-a1"]
+
+
+class TestLegacyVersion:
+ def test_legacy_version_is_deprecated(self):
+ with warnings.catch_warnings(record=True) as w:
+ LegacyVersion("some-legacy-version")
+ assert len(w) == 1
+ assert issubclass(w[0].category, DeprecationWarning)
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_valid_legacy_versions(self, version):
+ LegacyVersion(version)
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_str_repr(self, version):
+ assert str(LegacyVersion(version)) == version
+ assert repr(LegacyVersion(version)) == "<LegacyVersion({})>".format(
+ repr(version)
+ )
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_hash(self, version):
+ assert hash(LegacyVersion(version)) == hash(LegacyVersion(version))
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_public(self, version):
+ assert LegacyVersion(version).public == version
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_base_version(self, version):
+ assert LegacyVersion(version).base_version == version
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_epoch(self, version):
+ assert LegacyVersion(version).epoch == -1
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_release(self, version):
+ assert LegacyVersion(version).release is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_local(self, version):
+ assert LegacyVersion(version).local is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_pre(self, version):
+ assert LegacyVersion(version).pre is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_is_prerelease(self, version):
+ assert not LegacyVersion(version).is_prerelease
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_dev(self, version):
+ assert LegacyVersion(version).dev is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_is_devrelease(self, version):
+ assert not LegacyVersion(version).is_devrelease
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_post(self, version):
+ assert LegacyVersion(version).post is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_is_postrelease(self, version):
+ assert not LegacyVersion(version).is_postrelease
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of
+ # VERSIONS + LEGACY_VERSIONS that should be True for the given operator
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in VERSIONS + LEGACY_VERSIONS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [
+ (x, y, operator.ne)
+ for j, y in enumerate(VERSIONS + LEGACY_VERSIONS)
+ if i != j
+ ]
+ for i, x in enumerate(VERSIONS + LEGACY_VERSIONS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(LegacyVersion(left), LegacyVersion(right))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of
+ # VERSIONS + LEGACY_VERSIONS that should be False for the given
+ # operator
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [
+ [
+ (x, y, operator.eq)
+ for j, y in enumerate(VERSIONS + LEGACY_VERSIONS)
+ if i != j
+ ]
+ for i, x in enumerate(VERSIONS + LEGACY_VERSIONS)
+ ]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [[(x, x, operator.ne) for x in VERSIONS + LEGACY_VERSIONS]]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(LegacyVersion(left), LegacyVersion(right))
+
+ @pytest.mark.parametrize("op", ["lt", "le", "eq", "ge", "gt", "ne"])
+ def test_dunder_op_returns_notimplemented(self, op):
+ method = getattr(LegacyVersion, f"__{op}__")
+ assert method(LegacyVersion("1"), 1) is NotImplemented
+
+ @pytest.mark.parametrize(("op", "expected"), [("eq", False), ("ne", True)])
+ def test_compare_other(self, op, expected):
+ other = pretend.stub(**{f"__{op}__": lambda other: NotImplemented})
+
+ assert getattr(operator, op)(LegacyVersion("1"), other) is expected