diff options
author | Matthias Klose <doko@debian.org> | 2021-11-27 09:14:41 +0100 |
---|---|---|
committer | Matthias Klose <doko@debian.org> | 2021-11-27 09:14:41 +0100 |
commit | 3c577b5ace7676dc680c162a39de78d5bb6e9b5e (patch) | |
tree | 2f66ae2b61daa6fc4be752f3f295ea4e93221c78 |
Import python-packaging_21.3.orig.tar.gz
[dgit import orig python-packaging_21.3.orig.tar.gz]
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 @@ -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/ @@ -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 Binary files differnew file mode 100755 index 0000000..1dfd23f --- /dev/null +++ b/tests/manylinux/hello-world-armv7l-armel diff --git a/tests/manylinux/hello-world-armv7l-armhf b/tests/manylinux/hello-world-armv7l-armhf Binary files differnew file mode 100755 index 0000000..965ab30 --- /dev/null +++ b/tests/manylinux/hello-world-armv7l-armhf diff --git a/tests/manylinux/hello-world-invalid-class b/tests/manylinux/hello-world-invalid-class Binary files differnew file mode 100755 index 0000000..5e9899f --- /dev/null +++ b/tests/manylinux/hello-world-invalid-class diff --git a/tests/manylinux/hello-world-invalid-data b/tests/manylinux/hello-world-invalid-data Binary files differnew file mode 100755 index 0000000..2659b8e --- /dev/null +++ b/tests/manylinux/hello-world-invalid-data diff --git a/tests/manylinux/hello-world-invalid-magic b/tests/manylinux/hello-world-invalid-magic Binary files differnew file mode 100755 index 0000000..46066ad --- /dev/null +++ b/tests/manylinux/hello-world-invalid-magic diff --git a/tests/manylinux/hello-world-s390x-s390x b/tests/manylinux/hello-world-s390x-s390x Binary files differnew file mode 100644 index 0000000..c4e9578 --- /dev/null +++ b/tests/manylinux/hello-world-s390x-s390x diff --git a/tests/manylinux/hello-world-too-short b/tests/manylinux/hello-world-too-short Binary files differnew file mode 100644 index 0000000..4e5c039 --- /dev/null +++ b/tests/manylinux/hello-world-too-short diff --git a/tests/manylinux/hello-world-x86_64-amd64 b/tests/manylinux/hello-world-x86_64-amd64 Binary files differnew file mode 100644 index 0000000..c7f5b0b --- /dev/null +++ b/tests/manylinux/hello-world-x86_64-amd64 diff --git a/tests/manylinux/hello-world-x86_64-i386 b/tests/manylinux/hello-world-x86_64-i386 Binary files differnew file mode 100755 index 0000000..ff1d540 --- /dev/null +++ b/tests/manylinux/hello-world-x86_64-i386 diff --git a/tests/manylinux/hello-world-x86_64-x32 b/tests/manylinux/hello-world-x86_64-x32 Binary files differnew file mode 100755 index 0000000..daf85d3 --- /dev/null +++ b/tests/manylinux/hello-world-x86_64-x32 diff --git a/tests/musllinux/glibc-x86_64 b/tests/musllinux/glibc-x86_64 Binary files differnew file mode 100755 index 0000000..59996e2 --- /dev/null +++ b/tests/musllinux/glibc-x86_64 diff --git a/tests/musllinux/musl-aarch64 b/tests/musllinux/musl-aarch64 Binary files differnew file mode 100755 index 0000000..f6bcd38 --- /dev/null +++ b/tests/musllinux/musl-aarch64 diff --git a/tests/musllinux/musl-i386 b/tests/musllinux/musl-i386 Binary files differnew file mode 100755 index 0000000..2bbe495 --- /dev/null +++ b/tests/musllinux/musl-i386 diff --git a/tests/musllinux/musl-x86_64 b/tests/musllinux/musl-x86_64 Binary files differnew file mode 100755 index 0000000..d70261b --- /dev/null +++ b/tests/musllinux/musl-x86_64 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 |