Imported Upstream version 16.8 84/123984/1 upstream upstream/16.8
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 10 Apr 2017 01:51:25 +0000 (10:51 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 10 Apr 2017 01:51:46 +0000 (10:51 +0900)
Change-Id: I723561da3d718718fd6a82651a3787570ecb8a27
Signed-off-by: DongHun Kwak <dh0128.kwak@samsung.com>
48 files changed:
.coveragerc [new file with mode: 0644]
CHANGELOG.rst [new file with mode: 0644]
CONTRIBUTING.rst [new file with mode: 0644]
LICENSE [new file with mode: 0644]
LICENSE.APACHE [new file with mode: 0644]
LICENSE.BSD [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
PKG-INFO [new file with mode: 0644]
README.rst [new file with mode: 0644]
docs/Makefile [new file with mode: 0644]
docs/_static/.empty [new file with mode: 0644]
docs/changelog.rst [new file with mode: 0644]
docs/conf.py [new file with mode: 0644]
docs/development/getting-started.rst [new file with mode: 0644]
docs/development/index.rst [new file with mode: 0644]
docs/development/reviewing-patches.rst [new file with mode: 0644]
docs/development/submitting-patches.rst [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/markers.rst [new file with mode: 0644]
docs/requirements.rst [new file with mode: 0644]
docs/security.rst [new file with mode: 0644]
docs/specifiers.rst [new file with mode: 0644]
docs/utils.rst [new file with mode: 0644]
docs/version.rst [new file with mode: 0644]
packaging.egg-info/PKG-INFO [new file with mode: 0644]
packaging.egg-info/SOURCES.txt [new file with mode: 0644]
packaging.egg-info/dependency_links.txt [new file with mode: 0644]
packaging.egg-info/requires.txt [new file with mode: 0644]
packaging.egg-info/top_level.txt [new file with mode: 0644]
packaging/__about__.py [new file with mode: 0644]
packaging/__init__.py [new file with mode: 0644]
packaging/_compat.py [new file with mode: 0644]
packaging/_structures.py [new file with mode: 0644]
packaging/markers.py [new file with mode: 0644]
packaging/requirements.py [new file with mode: 0644]
packaging/specifiers.py [new file with mode: 0644]
packaging/utils.py [new file with mode: 0644]
packaging/version.py [new file with mode: 0644]
setup.cfg [new file with mode: 0644]
setup.py [new file with mode: 0644]
tests/__init__.py [new file with mode: 0644]
tests/test_markers.py [new file with mode: 0644]
tests/test_requirements.py [new file with mode: 0644]
tests/test_specifiers.py [new file with mode: 0644]
tests/test_structures.py [new file with mode: 0644]
tests/test_utils.py [new file with mode: 0644]
tests/test_version.py [new file with mode: 0644]
tox.ini [new file with mode: 0644]

diff --git a/.coveragerc b/.coveragerc
new file mode 100644 (file)
index 0000000..599863f
--- /dev/null
@@ -0,0 +1,8 @@
+[run]
+branch = True
+omit = packaging/_compat.py
+
+[report]
+exclude_lines =
+    @abc.abstractmethod
+    @abc.abstractproperty
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644 (file)
index 0000000..caa51cd
--- /dev/null
@@ -0,0 +1,164 @@
+Changelog
+---------
+
+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 arbitary 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.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644 (file)
index 0000000..d9d70ec
--- /dev/null
@@ -0,0 +1,23 @@
+Contributing to packaging
+=========================
+
+As an open source project, packaging welcomes contributions of many forms.
+
+Examples of contributions include:
+
+* Code patches
+* Documentation improvements
+* Bug reports and patch reviews
+
+Extensive contribution guidelines are available in the repository at
+``docs/development/index.rst``, or online at:
+
+https://packaging.pypa.io/en/latest/development/
+
+Security issues
+---------------
+
+To report a security issue, please follow the special `security reporting
+guidelines`_, do not report them in the public issue tracker.
+
+.. _`security reporting guidelines`: https://packaging.pypa.io/en/latest/security/
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..6f62d44
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,3 @@
+This software is made available under the terms of *either* of the licenses
+found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
+under the terms of *both* these licenses.
diff --git a/LICENSE.APACHE b/LICENSE.APACHE
new file mode 100644 (file)
index 0000000..4947287
--- /dev/null
@@ -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
\ No newline at end of file
diff --git a/LICENSE.BSD b/LICENSE.BSD
new file mode 100644 (file)
index 0000000..42ce7b7
--- /dev/null
@@ -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 (file)
index 0000000..c713aff
--- /dev/null
@@ -0,0 +1,14 @@
+include CHANGELOG.rst CONTRIBUTING.rst README.rst
+include LICENSE LICENSE.APACHE LICENSE.BSD
+
+include .coveragerc
+include tox.ini
+
+recursive-include docs *
+recursive-include tests *.py
+
+exclude .travis.yml
+exclude dev-requirements.txt
+
+prune docs/_build
+prune tasks
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644 (file)
index 0000000..1656f3e
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,217 @@
+Metadata-Version: 1.1
+Name: packaging
+Version: 16.8
+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 or Apache License, Version 2.0
+Description: packaging
+        =========
+        
+        Core utilities for Python packages
+        
+        
+        Documentation
+        -------------
+        
+        `documentation`_
+        
+        
+        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 `PyPA Code of Conduct`_.
+        
+        .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
+        
+        Changelog
+        ---------
+        
+        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 arbitary 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.
+        
+Platform: UNKNOWN
+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 :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..dac6a76
--- /dev/null
@@ -0,0 +1,31 @@
+packaging
+=========
+
+Core utilities for Python packages
+
+
+Documentation
+-------------
+
+`documentation`_
+
+
+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 `PyPA Code of Conduct`_.
+
+.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644 (file)
index 0000000..ec2771b
--- /dev/null
@@ -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."
\ No newline at end of file
diff --git a/docs/_static/.empty b/docs/_static/.empty
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644 (file)
index 0000000..565b052
--- /dev/null
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..ccc2963
--- /dev/null
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+# 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 __future__ import absolute_import, division, print_function
+
+import os
+import sys
+
+try:
+    import sphinx_rtd_theme
+except ImportError:
+    sphinx_rtd_theme = None
+
+# 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"
+copyright = "2014 Donald Stufft"
+
+# 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__"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ["_build"]
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+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.
+
+if sphinx_rtd_theme:
+    html_theme = "sphinx_rtd_theme"
+    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+else:
+    html_theme = "default"
+
+# 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 = {
+    "http://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 (file)
index 0000000..9c60a3d
--- /dev/null
@@ -0,0 +1,75 @@
+Getting started
+===============
+
+Working on packaging requires the installation of a small number of
+development dependencies. These are listed in ``dev-requirements.txt`` and they
+can be installed in a `virtualenv`_ using `pip`_. Once you've installed the
+dependencies, install packaging in ``editable`` mode. For example:
+
+.. code-block:: console
+
+    $ # Create a virtualenv and activate it
+    $ pip install --requirement dev-requirements.txt
+    $ pip install --editable .
+
+You are now ready to run the tests and build the documentation.
+
+Running tests
+~~~~~~~~~~~~~
+
+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
+
+    $ py.test
+    ...
+    62746 passed in 220.43 seconds
+
+This runs the tests with the default Python interpreter.
+
+You can also verify that the tests pass on other supported Python interpreters.
+For this we use `tox`_, which will automatically create a `virtualenv`_ for
+each supported Python version and run the tests. For example:
+
+.. code-block:: console
+
+    $ tox
+    ...
+    ERROR:   py26: InterpreterNotFound: python2.6
+     py27: commands succeeded
+    ERROR:   pypy: InterpreterNotFound: pypy
+    ERROR:   py32: InterpreterNotFound: python3.2
+     py33: commands succeeded
+     docs: commands succeeded
+     pep8: commands succeeded
+
+You may not have all the required Python versions installed, in which case you
+will see one or more ``InterpreterNotFound`` errors.
+
+
+Building documentation
+~~~~~~~~~~~~~~~~~~~~~~
+
+packaging documentation is stored in the ``docs/`` directory. It is
+written in `reStructured Text`_ and rendered using `Sphinx`_.
+
+Use `tox`_ to build the documentation. For example:
+
+.. code-block:: console
+
+    $ tox -e docs
+    ...
+    docs: commands succeeded
+    congratulations :)
+
+The HTML documentation index can now be found at
+``docs/_build/html/index.html``.
+
+.. _`pytest`: https://pypi.python.org/pypi/pytest
+.. _`tox`: https://pypi.python.org/pypi/tox
+.. _`virtualenv`: https://pypi.python.org/pypi/virtualenv
+.. _`pip`: https://pypi.python.org/pypi/pip
+.. _`sphinx`: https://pypi.python.org/pypi/Sphinx
+.. _`reStructured Text`: http://sphinx-doc.org/rest.html
\ No newline at end of file
diff --git a/docs/development/index.rst b/docs/development/index.rst
new file mode 100644 (file)
index 0000000..f81799a
--- /dev/null
@@ -0,0 +1,18 @@
+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
+
+.. _`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/reviewing-patches.rst b/docs/development/reviewing-patches.rst
new file mode 100644 (file)
index 0000000..4f1810c
--- /dev/null
@@ -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
\ No newline at end of file
diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst
new file mode 100644 (file)
index 0000000..f94cd4f
--- /dev/null
@@ -0,0 +1,80 @@
+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
+----
+
+When in doubt, refer to :pep:`8` for Python code. You can check if your code
+meets our automated requirements by running ``flake8`` against it. If you've
+installed the development requirements this will automatically use our
+configuration. You can also run the ``tox`` job with ``tox -e pep8``.
+
+`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.
+
+Additionally, every Python code file must contain
+
+.. code-block:: python
+
+    from __future__ import absolute_import, division, print_function
+
+
+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`_.
+
+
+.. _`Write comments as complete sentences.`: http://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 (file)
index 0000000..91897cc
--- /dev/null
@@ -0,0 +1,38 @@
+Welcome to packaging
+====================
+
+Core utilities for Python packages
+
+
+Installation
+------------
+
+You can install packaging with ``pip``:
+
+.. code-block:: console
+
+    $ pip install packaging
+
+
+API
+---
+
+.. toctree::
+    :maxdepth: 1
+
+    version
+    specifiers
+    markers
+    requirements
+    utils
+
+
+Project
+-------
+
+.. toctree::
+    :maxdepth: 2
+
+    development/index
+    security
+    changelog
diff --git a/docs/markers.rst b/docs/markers.rst
new file mode 100644 (file)
index 0000000..7613dae
--- /dev/null
@@ -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 a marker to see if the dependency is required
+    >>> 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 (file)
index 0000000..929fd58
--- /dev/null
@@ -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 list 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/security.rst b/docs/security.rst
new file mode 100644 (file)
index 0000000..9a0c10e
--- /dev/null
@@ -0,0 +1,12 @@
+Security
+========
+
+We take the security of packaging seriously. If you believe you've
+identified a security issue in it, please report it to
+``donald@stufft.io``. Message may be encrypted with PGP using key
+fingerprint ``7C6B 7C5D 5E2B 6356 A926 F04F 6E3C BCE9 3372 DCFA`` (this public
+key is available from most commonly-used key servers).
+
+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.
\ No newline at end of file
diff --git a/docs/specifiers.rst b/docs/specifiers.rst
new file mode 100644 (file)
index 0000000..729ecdc
--- /dev/null
@@ -0,0 +1,217 @@
+Specifiers
+==========
+
+.. currentmodule:: packaging.specifiers
+
+A core requirement of dealing with dependency 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 be attempted to be parsed as a PEP 440 specifier
+    (:class:`Specifier`) or as a legacy, setuptools style specifier
+    (: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 :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 :class:`LegacySpecifier`) instances in this specifier set.
+
+    .. method:: filter(iterable, prereleases=None)
+
+        Takes an iterable that can contain version strings, :class:`Version`,
+        and :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)
+
+    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/utils.rst b/docs/utils.rst
new file mode 100644 (file)
index 0000000..06cbbf7
--- /dev/null
@@ -0,0 +1,28 @@
+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'
diff --git a/docs/version.rst b/docs/version.rst
new file mode 100644 (file)
index 0000000..89f1d16
--- /dev/null
@@ -0,0 +1,145 @@
+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.is_prerelease
+    True
+    >>> v2.is_prerelease
+    False
+    >>> Version("french toast")
+    Traceback (most recent call last):
+        ...
+    InvalidVersion: Invalid version: 'french toast'
+    >>> Version("1.0").is_postrelease
+    False
+    >>> 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 :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:: local
+
+        A string representing the local version portion of this ``Version()``
+        if it has one, or ``None`` otherwise.
+
+    .. attribute:: is_prerelease
+
+        A boolean value indicating whether this :class:`Version` instance
+        represents a prerelease or a final release.
+
+    .. attribute:: is_postrelease
+
+        A boolean value indicating whether this :class:`Version` instance
+        represents a post-release.
+
+
+.. class:: LegacyVersion(version)
+
+    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.
+
+    .. 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:: 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:: is_prerelease
+
+        A boolean value indicating whether this :class:`LegacyVersion`
+        represents a prerelease or a final release. Since without `PEP 440`_
+        there is no concept of pre or final releases this will always be
+        `False` and exists for compatibility with :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/
diff --git a/packaging.egg-info/PKG-INFO b/packaging.egg-info/PKG-INFO
new file mode 100644 (file)
index 0000000..1656f3e
--- /dev/null
@@ -0,0 +1,217 @@
+Metadata-Version: 1.1
+Name: packaging
+Version: 16.8
+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 or Apache License, Version 2.0
+Description: packaging
+        =========
+        
+        Core utilities for Python packages
+        
+        
+        Documentation
+        -------------
+        
+        `documentation`_
+        
+        
+        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 `PyPA Code of Conduct`_.
+        
+        .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
+        
+        Changelog
+        ---------
+        
+        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 arbitary 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.
+        
+Platform: UNKNOWN
+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 :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
diff --git a/packaging.egg-info/SOURCES.txt b/packaging.egg-info/SOURCES.txt
new file mode 100644 (file)
index 0000000..cda2869
--- /dev/null
@@ -0,0 +1,47 @@
+.coveragerc
+CHANGELOG.rst
+CONTRIBUTING.rst
+LICENSE
+LICENSE.APACHE
+LICENSE.BSD
+MANIFEST.in
+README.rst
+setup.cfg
+setup.py
+tox.ini
+docs/Makefile
+docs/changelog.rst
+docs/conf.py
+docs/index.rst
+docs/markers.rst
+docs/requirements.rst
+docs/security.rst
+docs/specifiers.rst
+docs/utils.rst
+docs/version.rst
+docs/_static/.empty
+docs/development/getting-started.rst
+docs/development/index.rst
+docs/development/reviewing-patches.rst
+docs/development/submitting-patches.rst
+packaging/__about__.py
+packaging/__init__.py
+packaging/_compat.py
+packaging/_structures.py
+packaging/markers.py
+packaging/requirements.py
+packaging/specifiers.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_markers.py
+tests/test_requirements.py
+tests/test_specifiers.py
+tests/test_structures.py
+tests/test_utils.py
+tests/test_version.py
\ 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 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/packaging.egg-info/requires.txt b/packaging.egg-info/requires.txt
new file mode 100644 (file)
index 0000000..8a8af62
--- /dev/null
@@ -0,0 +1,2 @@
+pyparsing
+six
diff --git a/packaging.egg-info/top_level.txt b/packaging.egg-info/top_level.txt
new file mode 100644 (file)
index 0000000..748809f
--- /dev/null
@@ -0,0 +1 @@
+packaging
diff --git a/packaging/__about__.py b/packaging/__about__.py
new file mode 100644 (file)
index 0000000..95d330e
--- /dev/null
@@ -0,0 +1,21 @@
+# 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 __future__ import absolute_import, division, print_function
+
+__all__ = [
+    "__title__", "__summary__", "__uri__", "__version__", "__author__",
+    "__email__", "__license__", "__copyright__",
+]
+
+__title__ = "packaging"
+__summary__ = "Core utilities for Python packages"
+__uri__ = "https://github.com/pypa/packaging"
+
+__version__ = "16.8"
+
+__author__ = "Donald Stufft and individual contributors"
+__email__ = "donald@stufft.io"
+
+__license__ = "BSD or Apache License, Version 2.0"
+__copyright__ = "Copyright 2014-2016 %s" % __author__
diff --git a/packaging/__init__.py b/packaging/__init__.py
new file mode 100644 (file)
index 0000000..5ee6220
--- /dev/null
@@ -0,0 +1,14 @@
+# 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 __future__ import absolute_import, division, print_function
+
+from .__about__ import (
+    __author__, __copyright__, __email__, __license__, __summary__, __title__,
+    __uri__, __version__
+)
+
+__all__ = [
+    "__title__", "__summary__", "__uri__", "__version__", "__author__",
+    "__email__", "__license__", "__copyright__",
+]
diff --git a/packaging/_compat.py b/packaging/_compat.py
new file mode 100644 (file)
index 0000000..210bb80
--- /dev/null
@@ -0,0 +1,30 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import sys
+
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+# flake8: noqa
+
+if PY3:
+    string_types = str,
+else:
+    string_types = basestring,
+
+
+def with_metaclass(meta, *bases):
+    """
+    Create a base class with a metaclass.
+    """
+    # This requires a bit of explanation: the basic idea is to make a dummy
+    # metaclass for one level of class instantiation that replaces itself with
+    # the actual metaclass.
+    class metaclass(meta):
+        def __new__(cls, name, this_bases, d):
+            return meta(name, bases, d)
+    return type.__new__(metaclass, 'temporary_class', (), {})
diff --git a/packaging/_structures.py b/packaging/_structures.py
new file mode 100644 (file)
index 0000000..ccc2786
--- /dev/null
@@ -0,0 +1,68 @@
+# 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 __future__ import absolute_import, division, print_function
+
+
+class Infinity(object):
+
+    def __repr__(self):
+        return "Infinity"
+
+    def __hash__(self):
+        return hash(repr(self))
+
+    def __lt__(self, other):
+        return False
+
+    def __le__(self, other):
+        return False
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__)
+
+    def __ne__(self, other):
+        return not isinstance(other, self.__class__)
+
+    def __gt__(self, other):
+        return True
+
+    def __ge__(self, other):
+        return True
+
+    def __neg__(self):
+        return NegativeInfinity
+
+Infinity = Infinity()
+
+
+class NegativeInfinity(object):
+
+    def __repr__(self):
+        return "-Infinity"
+
+    def __hash__(self):
+        return hash(repr(self))
+
+    def __lt__(self, other):
+        return True
+
+    def __le__(self, other):
+        return True
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__)
+
+    def __ne__(self, other):
+        return not isinstance(other, self.__class__)
+
+    def __gt__(self, other):
+        return False
+
+    def __ge__(self, other):
+        return False
+
+    def __neg__(self):
+        return Infinity
+
+NegativeInfinity = NegativeInfinity()
diff --git a/packaging/markers.py b/packaging/markers.py
new file mode 100644 (file)
index 0000000..5fdf510
--- /dev/null
@@ -0,0 +1,301 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import operator
+import os
+import platform
+import sys
+
+from pyparsing import ParseException, ParseResults, stringStart, stringEnd
+from pyparsing import ZeroOrMore, Group, Forward, QuotedString
+from pyparsing import Literal as L  # noqa
+
+from ._compat import string_types
+from .specifiers import Specifier, InvalidSpecifier
+
+
+__all__ = [
+    "InvalidMarker", "UndefinedComparison", "UndefinedEnvironmentName",
+    "Marker", "default_environment",
+]
+
+
+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(object):
+
+    def __init__(self, value):
+        self.value = value
+
+    def __str__(self):
+        return str(self.value)
+
+    def __repr__(self):
+        return "<{0}({1!r})>".format(self.__class__.__name__, str(self))
+
+    def serialize(self):
+        raise NotImplementedError
+
+
+class Variable(Node):
+
+    def serialize(self):
+        return str(self)
+
+
+class Value(Node):
+
+    def serialize(self):
+        return '"{0}"'.format(self)
+
+
+class Op(Node):
+
+    def serialize(self):
+        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")
+)
+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):
+    if isinstance(results, ParseResults):
+        return [_coerce_parse_result(i) for i in results]
+    else:
+        return results
+
+
+def _format_marker(marker, first=True):
+    assert isinstance(marker, (list, tuple, string_types))
+
+    # 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 = {
+    "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, op, rhs):
+    try:
+        spec = Specifier("".join([op.serialize(), rhs]))
+    except InvalidSpecifier:
+        pass
+    else:
+        return spec.contains(lhs)
+
+    oper = _operators.get(op.serialize())
+    if oper is None:
+        raise UndefinedComparison(
+            "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs)
+        )
+
+    return oper(lhs, rhs)
+
+
+_undefined = object()
+
+
+def _get_env(environment, name):
+    value = environment.get(name, _undefined)
+
+    if value is _undefined:
+        raise UndefinedEnvironmentName(
+            "{0!r} does not exist in evaluation environment.".format(name)
+        )
+
+    return value
+
+
+def _evaluate_markers(markers, environment):
+    groups = [[]]
+
+    for marker in markers:
+        assert isinstance(marker, (list, tuple, string_types))
+
+        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):
+    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():
+    if hasattr(sys, 'implementation'):
+        iver = format_full_version(sys.implementation.version)
+        implementation_name = sys.implementation.name
+    else:
+        iver = '0'
+        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": platform.python_version()[:3],
+        "sys_platform": sys.platform,
+    }
+
+
+class Marker(object):
+
+    def __init__(self, marker):
+        try:
+            self._markers = _coerce_parse_result(MARKER.parseString(marker))
+        except ParseException as e:
+            err_str = "Invalid marker: {0!r}, parse error at {1!r}".format(
+                marker, marker[e.loc:e.loc + 8])
+            raise InvalidMarker(err_str)
+
+    def __str__(self):
+        return _format_marker(self._markers)
+
+    def __repr__(self):
+        return "<Marker({0!r})>".format(str(self))
+
+    def evaluate(self, environment=None):
+        """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/requirements.py b/packaging/requirements.py
new file mode 100644 (file)
index 0000000..a1bb414
--- /dev/null
@@ -0,0 +1,127 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import string
+import re
+
+from pyparsing import stringStart, stringEnd, originalTextFor, ParseException
+from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
+from pyparsing import Literal as L  # noqa
+from six.moves.urllib import parse as urlparse
+
+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_SEPERATOR = SEMICOLON
+MARKER = MARKER_SEPERATOR + 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
+
+
+class Requirement(object):
+    """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):
+        try:
+            req = REQUIREMENT.parseString(requirement_string)
+        except ParseException as e:
+            raise InvalidRequirement(
+                "Invalid requirement, parse error at \"{0!r}\"".format(
+                    requirement_string[e.loc:e.loc + 8]))
+
+        self.name = req.name
+        if req.url:
+            parsed_url = urlparse.urlparse(req.url)
+            if not (parsed_url.scheme and parsed_url.netloc) or (
+                    not parsed_url.scheme and not parsed_url.netloc):
+                raise InvalidRequirement("Invalid URL given")
+            self.url = req.url
+        else:
+            self.url = None
+        self.extras = set(req.extras.asList() if req.extras else [])
+        self.specifier = SpecifierSet(req.specifier)
+        self.marker = req.marker if req.marker else None
+
+    def __str__(self):
+        parts = [self.name]
+
+        if self.extras:
+            parts.append("[{0}]".format(",".join(sorted(self.extras))))
+
+        if self.specifier:
+            parts.append(str(self.specifier))
+
+        if self.url:
+            parts.append("@ {0}".format(self.url))
+
+        if self.marker:
+            parts.append("; {0}".format(self.marker))
+
+        return "".join(parts)
+
+    def __repr__(self):
+        return "<Requirement({0!r})>".format(str(self))
diff --git a/packaging/specifiers.py b/packaging/specifiers.py
new file mode 100644 (file)
index 0000000..7f5a76c
--- /dev/null
@@ -0,0 +1,774 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import abc
+import functools
+import itertools
+import re
+
+from ._compat import string_types, with_metaclass
+from .version import Version, LegacyVersion, parse
+
+
+class InvalidSpecifier(ValueError):
+    """
+    An invalid specifier was found, users should refer to PEP 440.
+    """
+
+
+class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
+
+    @abc.abstractmethod
+    def __str__(self):
+        """
+        Returns the str representation of this Specifier like object. This
+        should be representative of the Specifier itself.
+        """
+
+    @abc.abstractmethod
+    def __hash__(self):
+        """
+        Returns a hash value for this Specifier like object.
+        """
+
+    @abc.abstractmethod
+    def __eq__(self, other):
+        """
+        Returns a boolean representing whether or not the two Specifier like
+        objects are equal.
+        """
+
+    @abc.abstractmethod
+    def __ne__(self, other):
+        """
+        Returns a boolean representing whether or not the two Specifier like
+        objects are not equal.
+        """
+
+    @abc.abstractproperty
+    def prereleases(self):
+        """
+        Returns whether or not pre-releases as a whole are allowed by this
+        specifier.
+        """
+
+    @prereleases.setter
+    def prereleases(self, value):
+        """
+        Sets whether or not pre-releases as a whole are allowed by this
+        specifier.
+        """
+
+    @abc.abstractmethod
+    def contains(self, item, prereleases=None):
+        """
+        Determines if the given item is contained within this specifier.
+        """
+
+    @abc.abstractmethod
+    def filter(self, iterable, prereleases=None):
+        """
+        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 = {}
+
+    def __init__(self, spec="", prereleases=None):
+        match = self._regex.search(spec)
+        if not match:
+            raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec))
+
+        self._spec = (
+            match.group("operator").strip(),
+            match.group("version").strip(),
+        )
+
+        # Store whether or not this Specifier should accept prereleases
+        self._prereleases = prereleases
+
+    def __repr__(self):
+        pre = (
+            ", prereleases={0!r}".format(self.prereleases)
+            if self._prereleases is not None
+            else ""
+        )
+
+        return "<{0}({1!r}{2})>".format(
+            self.__class__.__name__,
+            str(self),
+            pre,
+        )
+
+    def __str__(self):
+        return "{0}{1}".format(*self._spec)
+
+    def __hash__(self):
+        return hash(self._spec)
+
+    def __eq__(self, other):
+        if isinstance(other, string_types):
+            try:
+                other = self.__class__(other)
+            except InvalidSpecifier:
+                return NotImplemented
+        elif not isinstance(other, self.__class__):
+            return NotImplemented
+
+        return self._spec == other._spec
+
+    def __ne__(self, other):
+        if isinstance(other, string_types):
+            try:
+                other = self.__class__(other)
+            except InvalidSpecifier:
+                return NotImplemented
+        elif not isinstance(other, self.__class__):
+            return NotImplemented
+
+        return self._spec != other._spec
+
+    def _get_operator(self, op):
+        return getattr(self, "_compare_{0}".format(self._operators[op]))
+
+    def _coerce_version(self, version):
+        if not isinstance(version, (LegacyVersion, Version)):
+            version = parse(version)
+        return version
+
+    @property
+    def operator(self):
+        return self._spec[0]
+
+    @property
+    def version(self):
+        return self._spec[1]
+
+    @property
+    def prereleases(self):
+        return self._prereleases
+
+    @prereleases.setter
+    def prereleases(self, value):
+        self._prereleases = value
+
+    def __contains__(self, item):
+        return self.contains(item)
+
+    def contains(self, item, prereleases=None):
+        # 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")
+        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 item.is_prerelease and not prereleases:
+            return False
+
+        # Actually do the comparison to determine if this item is contained
+        # within this Specifier or not.
+        return self._get_operator(self.operator)(item, self.version)
+
+    def filter(self, iterable, prereleases=None):
+        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 incase 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 begining.
+                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 _coerce_version(self, version):
+        if not isinstance(version, LegacyVersion):
+            version = LegacyVersion(str(version))
+        return version
+
+    def _compare_equal(self, prospective, spec):
+        return prospective == self._coerce_version(spec)
+
+    def _compare_not_equal(self, prospective, spec):
+        return prospective != self._coerce_version(spec)
+
+    def _compare_less_than_equal(self, prospective, spec):
+        return prospective <= self._coerce_version(spec)
+
+    def _compare_greater_than_equal(self, prospective, spec):
+        return prospective >= self._coerce_version(spec)
+
+    def _compare_less_than(self, prospective, spec):
+        return prospective < self._coerce_version(spec)
+
+    def _compare_greater_than(self, prospective, spec):
+        return prospective > self._coerce_version(spec)
+
+
+def _require_version_compare(fn):
+    @functools.wraps(fn)
+    def wrapped(self, prospective, spec):
+        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, spec):
+        # 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 post and dev releases and we want to treat the pre-release as
+        # it's own separate segment.
+        prefix = ".".join(
+            list(
+                itertools.takewhile(
+                    lambda x: (not x.startswith("post") and not
+                               x.startswith("dev")),
+                    _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, spec):
+        # 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.
+            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.
+            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.
+            prospective = prospective[:len(spec)]
+
+            # Pad out our two sides with zeros so that they both equal the same
+            # length.
+            spec, prospective = _pad_version(spec, prospective)
+        else:
+            # Convert our spec string into a Version
+            spec = 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.local:
+                prospective = Version(prospective.public)
+
+        return prospective == spec
+
+    @_require_version_compare
+    def _compare_not_equal(self, prospective, spec):
+        return not self._compare_equal(prospective, spec)
+
+    @_require_version_compare
+    def _compare_less_than_equal(self, prospective, spec):
+        return prospective <= Version(spec)
+
+    @_require_version_compare
+    def _compare_greater_than_equal(self, prospective, spec):
+        return prospective >= Version(spec)
+
+    @_require_version_compare
+    def _compare_less_than(self, prospective, spec):
+        # Convert our spec to a Version instance, since we'll want to work with
+        # it as a version.
+        spec = Version(spec)
+
+        # 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, spec):
+        # Convert our spec to a Version instance, since we'll want to work with
+        # it as a version.
+        spec = Version(spec)
+
+        # 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 techincally 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, spec):
+        return str(prospective).lower() == str(spec).lower()
+
+    @property
+    def prereleases(self):
+        # 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):
+        self._prereleases = value
+
+
+_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
+
+
+def _version_split(version):
+    result = []
+    for item in version.split("."):
+        match = _prefix_regex.search(item)
+        if match:
+            result.extend(match.groups())
+        else:
+            result.append(item)
+    return result
+
+
+def _pad_version(left, right):
+    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="", prereleases=None):
+        # Split on , to break each indidivual specifier into it's own item, and
+        # strip each item to remove leading/trailing whitespace.
+        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()
+        for specifier in 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):
+        pre = (
+            ", prereleases={0!r}".format(self.prereleases)
+            if self._prereleases is not None
+            else ""
+        )
+
+        return "<SpecifierSet({0!r}{1})>".format(str(self), pre)
+
+    def __str__(self):
+        return ",".join(sorted(str(s) for s in self._specs))
+
+    def __hash__(self):
+        return hash(self._specs)
+
+    def __and__(self, other):
+        if isinstance(other, string_types):
+            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):
+        if isinstance(other, string_types):
+            other = SpecifierSet(other)
+        elif isinstance(other, _IndividualSpecifier):
+            other = SpecifierSet(str(other))
+        elif not isinstance(other, SpecifierSet):
+            return NotImplemented
+
+        return self._specs == other._specs
+
+    def __ne__(self, other):
+        if isinstance(other, string_types):
+            other = SpecifierSet(other)
+        elif isinstance(other, _IndividualSpecifier):
+            other = SpecifierSet(str(other))
+        elif not isinstance(other, SpecifierSet):
+            return NotImplemented
+
+        return self._specs != other._specs
+
+    def __len__(self):
+        return len(self._specs)
+
+    def __iter__(self):
+        return iter(self._specs)
+
+    @property
+    def prereleases(self):
+        # 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):
+        self._prereleases = value
+
+    def __contains__(self, item):
+        return self.contains(item)
+
+    def contains(self, item, prereleases=None):
+        # 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, prereleases=None):
+        # 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 = []
+            found_prereleases = []
+
+            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/utils.py b/packaging/utils.py
new file mode 100644 (file)
index 0000000..942387c
--- /dev/null
@@ -0,0 +1,14 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import re
+
+
+_canonicalize_regex = re.compile(r"[-_.]+")
+
+
+def canonicalize_name(name):
+    # This is taken from PEP 503.
+    return _canonicalize_regex.sub("-", name).lower()
diff --git a/packaging/version.py b/packaging/version.py
new file mode 100644 (file)
index 0000000..83b5ee8
--- /dev/null
@@ -0,0 +1,393 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import collections
+import itertools
+import re
+
+from ._structures import Infinity
+
+
+__all__ = [
+    "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"
+]
+
+
+_Version = collections.namedtuple(
+    "_Version",
+    ["epoch", "release", "dev", "pre", "post", "local"],
+)
+
+
+def parse(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(object):
+
+    def __hash__(self):
+        return hash(self._key)
+
+    def __lt__(self, other):
+        return self._compare(other, lambda s, o: s < o)
+
+    def __le__(self, other):
+        return self._compare(other, lambda s, o: s <= o)
+
+    def __eq__(self, other):
+        return self._compare(other, lambda s, o: s == o)
+
+    def __ge__(self, other):
+        return self._compare(other, lambda s, o: s >= o)
+
+    def __gt__(self, other):
+        return self._compare(other, lambda s, o: s > o)
+
+    def __ne__(self, other):
+        return self._compare(other, lambda s, o: s != o)
+
+    def _compare(self, other, method):
+        if not isinstance(other, _BaseVersion):
+            return NotImplemented
+
+        return method(self._key, other._key)
+
+
+class LegacyVersion(_BaseVersion):
+
+    def __init__(self, version):
+        self._version = str(version)
+        self._key = _legacy_cmpkey(self._version)
+
+    def __str__(self):
+        return self._version
+
+    def __repr__(self):
+        return "<LegacyVersion({0})>".format(repr(str(self)))
+
+    @property
+    def public(self):
+        return self._version
+
+    @property
+    def base_version(self):
+        return self._version
+
+    @property
+    def local(self):
+        return None
+
+    @property
+    def is_prerelease(self):
+        return False
+
+    @property
+    def is_postrelease(self):
+        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):
+    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):
+    # 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 = []
+    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)
+    parts = tuple(parts)
+
+    return epoch, 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):
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion("Invalid version: '{0}'".format(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):
+        return "<Version({0})>".format(repr(str(self)))
+
+    def __str__(self):
+        parts = []
+
+        # Epoch
+        if self._version.epoch != 0:
+            parts.append("{0}!".format(self._version.epoch))
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self._version.release))
+
+        # Pre-release
+        if self._version.pre is not None:
+            parts.append("".join(str(x) for x in self._version.pre))
+
+        # Post-release
+        if self._version.post is not None:
+            parts.append(".post{0}".format(self._version.post[1]))
+
+        # Development release
+        if self._version.dev is not None:
+            parts.append(".dev{0}".format(self._version.dev[1]))
+
+        # Local version segment
+        if self._version.local is not None:
+            parts.append(
+                "+{0}".format(".".join(str(x) for x in self._version.local))
+            )
+
+        return "".join(parts)
+
+    @property
+    def public(self):
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self):
+        parts = []
+
+        # Epoch
+        if self._version.epoch != 0:
+            parts.append("{0}!".format(self._version.epoch))
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self._version.release))
+
+        return "".join(parts)
+
+    @property
+    def local(self):
+        version_string = str(self)
+        if "+" in version_string:
+            return version_string.split("+", 1)[1]
+
+    @property
+    def is_prerelease(self):
+        return bool(self._version.dev or self._version.pre)
+
+    @property
+    def is_postrelease(self):
+        return bool(self._version.post)
+
+
+def _parse_letter_version(letter, number):
+    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)
+
+
+_local_version_seperators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local):
+    """
+    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_seperators.split(local)
+        )
+
+
+def _cmpkey(epoch, release, pre, post, dev, local):
+    # 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 = -Infinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        pre = Infinity
+
+    # Versions without a post segment should sort before those with one.
+    if post is None:
+        post = -Infinity
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        dev = Infinity
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        local = -Infinity
+    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 (-Infinity, i)
+            for i in local
+        )
+
+    return epoch, release, pre, post, dev, local
diff --git a/setup.cfg b/setup.cfg
new file mode 100644 (file)
index 0000000..6f08d0e
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..77c69d4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,74 @@
+#!/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.
+from __future__ import absolute_import, division, print_function
+
+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,
+    license=about["__license__"],
+    url=about["__uri__"],
+
+    author=about["__author__"],
+    author_email=about["__email__"],
+
+    install_requires=["pyparsing", "six"],
+
+    classifiers=[
+        "Intended Audience :: Developers",
+
+        "License :: OSI Approved :: Apache Software License",
+        "License :: OSI Approved :: BSD License",
+
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.2",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+    ],
+
+    packages=[
+        "packaging",
+    ],
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..0e11301
--- /dev/null
@@ -0,0 +1,4 @@
+# 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 __future__ import absolute_import, division, print_function
diff --git a/tests/test_markers.py b/tests/test_markers.py
new file mode 100644 (file)
index 0000000..0e2d96a
--- /dev/null
@@ -0,0 +1,374 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import collections
+import itertools
+import os
+import platform
+import sys
+
+import pretend
+import pytest
+
+from packaging.markers import (
+    Node, InvalidMarker, UndefinedComparison, UndefinedEnvironmentName, Marker,
+    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)) == "<Node({0!r})>".format(str(value))
+
+    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:
+
+    @pytest.mark.skipif(hasattr(sys, 'implementation'),
+                        reason='sys.implementation does exist')
+    def test_matches_expected_no_sys_implementation(self):
+        environment = default_environment()
+
+        assert environment == {
+            "implementation_name": "",
+            "implementation_version": "0",
+            "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": platform.python_version()[:3],
+            "sys_platform": sys.platform,
+        }
+
+    @pytest.mark.skipif(not hasattr(sys, 'implementation'),
+                        reason='sys.implementation does not exist')
+    def test_matches_expected_deleted_sys_implementation(self, monkeypatch):
+        monkeypatch.delattr(sys, "implementation")
+
+        environment = default_environment()
+
+        assert environment == {
+            "implementation_name": "",
+            "implementation_version": "0",
+            "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": platform.python_version()[:3],
+            "sys_platform": sys.platform,
+        }
+
+    @pytest.mark.skipif(not hasattr(sys, 'implementation'),
+                        reason='sys.implementation does not exist')
+    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": platform.python_version()[:3],
+            "sys_platform": sys.platform,
+        }
+
+    @pytest.mark.skipif(hasattr(sys, 'implementation'),
+                        reason='sys.implementation does exist')
+    def test_monkeypatch_sys_implementation(self, monkeypatch):
+        monkeypatch.setattr(
+            sys, "implementation",
+            pretend.stub(version=FakeVersionInfo(3, 4, 2, "final", 0),
+                         name="linux"),
+            raising=False)
+
+        environment = default_environment()
+        assert environment == {
+            "implementation_name": "linux",
+            "implementation_version": "3.4.2",
+            "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": platform.python_version()[:3],
+            "sys_platform": sys.platform,
+        }
+
+    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",
+        [
+            "{0} {1} {2!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) == "<Marker({0!r})>".format(str(m))
+
+    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"),
+        [
+            ("os_name == '{0}'".format(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",
+        [
+            "{0} {1} {2!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"),
+        [
+            ("os.name == '{0}'".format(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",
+        [
+            "{0} {1} {2!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_requirements.py b/tests/test_requirements.py
new file mode 100644 (file)
index 0000000..536e712
--- /dev/null
@@ -0,0 +1,172 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import pytest
+
+from packaging.markers import Marker
+from packaging.requirements import InvalidRequirement, Requirement, URL
+from packaging.requirements import URL_AND_MARKER
+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_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):
+            Requirement("name @ gopher:/foo/com")
+
+    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_comparsion_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
diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py
new file mode 100644 (file)
index 0000000..adc10e5
--- /dev/null
@@ -0,0 +1,1043 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import itertools
+import operator
+
+import pytest
+
+from packaging.specifiers import (
+    InvalidSpecifier, LegacySpecifier, Specifier, SpecifierSet,
+)
+from packaging.version import LegacyVersion, Version, parse
+
+from .test_version import VERSIONS, LEGACY_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) == "<Specifier({0})>".format(repr(expected))
+
+    @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", "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"),
+
+                # 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 = set(str(item) for item in spec)
+        assert items == set(expected_items)
+
+
+class TestLegacySpecifier:
+
+    @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) == "<SpecifierSet({0})>".format(repr(expected))
+
+    @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)
+
+    def test_comparison_non_specifier(self):
+        assert SpecifierSet("==1.0") != 12
+        assert not SpecifierSet("==1.0") == 12
diff --git a/tests/test_structures.py b/tests/test_structures.py
new file mode 100644 (file)
index 0000000..33b1896
--- /dev/null
@@ -0,0 +1,60 @@
+# 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 __future__ import absolute_import, division, print_function
+
+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_infinty_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_utils.py b/tests/test_utils.py
new file mode 100644 (file)
index 0000000..1c96b31
--- /dev/null
@@ -0,0 +1,27 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import pytest
+
+from packaging.utils import canonicalize_name
+
+
+@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
diff --git a/tests/test_version.py b/tests/test_version.py
new file mode 100644 (file)
index 0000000..7d1a7bb
--- /dev/null
@@ -0,0 +1,600 @@
+# 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 __future__ import absolute_import, division, print_function
+
+import itertools
+import operator
+
+import pretend
+import pytest
+
+from packaging.version import Version, LegacyVersion, InvalidVersion, 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)) ==
+                "<Version({0})>".format(repr(expected)))
+
+    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.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.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", "local"),
+        [
+            ("1.0", 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", "expected"),
+        [
+            ("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", "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", "expected"), [("eq", False), ("ne", True)])
+    def test_compare_other(self, op, expected):
+        other = pretend.stub(
+            **{"__{0}__".format(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")]
+
+
+LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1-0", "2.0-a1"]
+
+
+class TestLegacyVersion:
+
+    @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({0})>".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_local(self, version):
+        assert LegacyVersion(version).local 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_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", "expected"), [("eq", False), ("ne", True)])
+    def test_compare_other(self, op, expected):
+        other = pretend.stub(
+            **{"__{0}__".format(op): lambda other: NotImplemented}
+        )
+
+        assert getattr(operator, op)(LegacyVersion("1"), other) is expected
diff --git a/tox.ini b/tox.ini
new file mode 100644 (file)
index 0000000..a974a77
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,63 @@
+[tox]
+envlist = py26,py27,pypy,py32,py33,py34,docs,pep8,py2pep8
+
+[testenv]
+deps =
+    coverage
+    pretend
+    pytest
+commands =
+    python -m coverage run --source packaging/ -m pytest --strict {posargs}
+    python -m coverage report -m --fail-under 100
+install_command =
+    pip install --find-links https://wheels.caremad.io/ {opts} {packages}
+
+# Python 2.6 doesn't support python -m on a package.
+[testenv:py26]
+commands =
+    python -m coverage.__main__ run --source packaging/ -m pytest --strict {posargs}
+    python -m coverage.__main__ report -m --fail-under 100
+
+# coverage.py doesn't support Python 3.2 anymore.
+[testenv:py32]
+commands =
+    py.test --strict {posargs}
+
+[testenv:pypy]
+commands =
+    py.test --capture=no --strict {posargs}
+
+[testenv:docs]
+deps =
+    sphinx
+    sphinx_rtd_theme
+commands =
+    sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html
+    sphinx-build -W -b latex -d {envtmpdir}/doctrees docs docs/_build/latex
+    sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html
+
+[testenv:pep8]
+basepython = python3.2
+deps =
+    flake8
+    pep8-naming
+commands = flake8 .
+
+[testenv:py2pep8]
+basepython = python2.6
+deps =
+    flake8
+    pep8-naming
+commands = flake8 .
+
+[testenv:packaging]
+deps =
+    check-manifest
+    readme_renderer
+commands =
+    check-manifest
+    python setup.py check --metadata --restructuredtext --strict
+
+[flake8]
+exclude = .tox,*.egg
+select = E,W,F,N