From 7a194d174b84bd0ce1d63db46627f79a6db4247e Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Fri, 16 May 2014 07:52:07 -0400 Subject: [PATCH 01/17] Require 100% test coverage --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1735308ad..c543c3057 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,8 @@ deps = coverage pytest commands = - coverage run --source=packaging/,tests/ -m pytest --capture=no --strict {posargs} - coverage report -m + coverage run --source=packaging/ -m pytest --capture=no --strict {posargs} + coverage report -m --fail-under 100 install_command = pip install --find-links https://wheels.caremad.io/ {opts} {packages} From fbd039c03fc24a33213a1177f4aad0ad02e7b178 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Fri, 16 May 2014 07:51:46 -0400 Subject: [PATCH 02/17] Implement PEP 440 versions --- docs/index.rst | 14 ++ docs/version.rst | 69 ++++++++ packaging/_structures.py | 78 +++++++++ packaging/version.py | 235 +++++++++++++++++++++++++++ tests/test_structures.py | 70 +++++++++ tests/test_version.py | 331 +++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 7 files changed, 798 insertions(+) create mode 100644 docs/version.rst create mode 100644 packaging/_structures.py create mode 100644 packaging/version.py create mode 100644 tests/test_structures.py create mode 100644 tests/test_version.py diff --git a/docs/index.rst b/docs/index.rst index 7f1c1293b..a63afb6c6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,12 +6,26 @@ Core utilities for Python packages Installation ------------ + You can install packaging with ``pip``: .. code-block:: console $ pip install packaging + +API +--- + +.. toctree:: + :maxdepth: 1 + + version + + +Project +------- + .. toctree:: :maxdepth: 2 diff --git a/docs/version.rst b/docs/version.rst new file mode 100644 index 000000000..70dd0f8d9 --- /dev/null +++ b/docs/version.rst @@ -0,0 +1,69 @@ +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 + >>> v1 = Version("1.0a5") + >>> v2 = Version("1.0") + >>> v1 + + >>> v2 + + >>> v1 < v2 + True + >>> v1.is_prerelease + True + >>> v2.is_prerelease + False + >>> Version("french toast") + Traceback (most recent call last): + ... + InvalidVersion: Invalid version: 'french toast' + + +Reference +--------- + +.. 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:: 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. + + +.. class:: InvalidVersion: + + Raised when attempting to create a :class:`Version` with a version string + that does not conform to `PEP 440`_. + + +.. _`PEP 440`: https://www.python.org/dev/peps/pep-0440/ diff --git a/packaging/_structures.py b/packaging/_structures.py new file mode 100644 index 000000000..0ae9bb52a --- /dev/null +++ b/packaging/_structures.py @@ -0,0 +1,78 @@ +# Copyright 2014 Donald Stufft +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +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/version.py b/packaging/version.py new file mode 100644 index 000000000..a8e529252 --- /dev/null +++ b/packaging/version.py @@ -0,0 +1,235 @@ +# Copyright 2014 Donald Stufft +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import, division, print_function + +import collections +import itertools +import re + +from ._structures import Infinity + +# TODO: We deviate from the spec in that we have no implicit specifier operator +# instead we mandate all specifiers must include an explicit operator. + +__all__ = ["Version"] + + +_Version = collections.namedtuple( + "_Version", + ["epoch", "release", "dev", "pre", "post", "local"], +) + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class Version(object): + + _regex = re.compile( + r""" + ^ + (?: + (?:(?P[0-9]+):)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                         # pre release
+                (?P(a|b|c|rc))        #  - pre-release letter
+                (?P[0-9]+)            #  - pre-release number
+            )?
+            (?:\.post(?P[0-9]+))?      # post release
+            (?:\.dev(?P[0-9]+))?        # dev release
+        )
+        (?:\+(?P[a-z0-9]+(?:[a-z0-9\.]*[a-z0-9])?))? # local version
+        $
+        """,
+        re.VERBOSE,
+    )
+
+    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=_parse_dot_version(match.group("release")),
+            pre=_parse_pre_version(match.group("pre_l"), match.group("pre_n")),
+            post=int(match.group("post")) if match.group("post") else None,
+            dev=int(match.group("dev")) if match.group("dev") else None,
+            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 "".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))
+
+        # Development release
+        if self._version.dev is not None:
+            parts.append(".dev{0}".format(self._version.dev))
+
+        # 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)
+
+    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, Version):
+            return NotImplemented
+
+        return method(self._key, other._key)
+
+    @property
+    def public(self):
+        return str(self).split("+", 1)[0]
+
+    @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)
+
+
+def _parse_dot_version(part):
+    """
+    Takes a string like "1.0.4.0" and turns it into (1, 0, 4).
+    """
+    return tuple(
+        reversed(
+            list(
+                itertools.dropwhile(
+                    lambda x: x == 0,
+                    reversed(list(int(i) for i in part.split("."))),
+                )
+            )
+        )
+    )
+
+
+def _parse_pre_version(letter, number):
+    if letter and number:
+        # We consider the "rc" form of a pre-release to be long-form for the
+        # "c" form, thus we normalize "rc" to "c" so we can properly compare
+        # them as equal.
+        if letter == "rc":
+            letter = "c"
+        return (letter, int(number))
+
+
+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 if not part.isdigit() else int(part)
+            for part in local.split(".")
+        )
+
+
+def _cmpkey(epoch, release, pre, post, dev, local):
+    # 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/tests/test_structures.py b/tests/test_structures.py
new file mode 100644
index 000000000..86597c1a3
--- /dev/null
+++ b/tests/test_structures.py
@@ -0,0 +1,70 @@
+# Copyright 2014 Donald Stufft
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+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_version.py b/tests/test_version.py
new file mode 100644
index 000000000..3d1236342
--- /dev/null
+++ b/tests/test_version.py
@@ -0,0 +1,331 @@
+# Copyright 2014 Donald Stufft
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import absolute_import, division, print_function
+
+import itertools
+import operator
+
+import pretend
+import pytest
+
+from packaging.version import Version, InvalidVersion
+
+
+# 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.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",
+
+    # 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.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",
+]
+
+
+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+a+",
+            "1.0++",
+            "1.0+_foobar",
+            "1.0+foo&asd",
+            "1.0+1+1",
+            "1.0+1_1",
+        ]
+    )
+    def test_invalid_versions(self, version):
+        with pytest.raises(InvalidVersion):
+            Version(version)
+
+    @pytest.mark.parametrize(
+        ("version", "expected"),
+        [
+            ("1.0.dev456", "1.dev456"),
+            ("1.0a1", "1a1"),
+            ("1.0a2.dev456", "1a2.dev456"),
+            ("1.0a12.dev456", "1a12.dev456"),
+            ("1.0a12", "1a12"),
+            ("1.0b1.dev456", "1b1.dev456"),
+            ("1.0b2", "1b2"),
+            ("1.0b2.post345.dev456", "1b2.post345.dev456"),
+            ("1.0b2.post345", "1b2.post345"),
+            ("1.0c1.dev456", "1c1.dev456"),
+            ("1.0c1", "1c1"),
+            ("1.0", "1"),
+            ("1.0.post456.dev34", "1.post456.dev34"),
+            ("1.0.post456", "1.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.dev456"),
+            ("7:1.0a1", "7:1a1"),
+            ("7:1.0a2.dev456", "7:1a2.dev456"),
+            ("7:1.0a12.dev456", "7:1a12.dev456"),
+            ("7:1.0a12", "7:1a12"),
+            ("7:1.0b1.dev456", "7:1b1.dev456"),
+            ("7:1.0b2", "7:1b2"),
+            ("7:1.0b2.post345.dev456", "7:1b2.post345.dev456"),
+            ("7:1.0b2.post345", "7:1b2.post345"),
+            ("7:1.0c1.dev456", "7:1c1.dev456"),
+            ("7:1.0c1", "7:1c1"),
+            ("7:1.0", "7:1"),
+            ("7:1.0.post456.dev34", "7:1.post456.dev34"),
+            ("7:1.0.post456", "7:1.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))
+                == "".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"),
+            ("1.0.dev6", "1.dev6"),
+            ("1.0a1", "1a1"),
+            ("1.0a1.post5", "1a1.post5"),
+            ("1.0a1.post5.dev6", "1a1.post5.dev6"),
+            ("1.0rc4", "1c4"),
+            ("1.0.post5", "1.post5"),
+            ("1:1.0", "1:1"),
+            ("1:1.0.dev6", "1:1.dev6"),
+            ("1:1.0a1", "1:1a1"),
+            ("1:1.0a1.post5", "1:1a1.post5"),
+            ("1:1.0a1.post5.dev6", "1:1a1.post5.dev6"),
+            ("1:1.0rc4", "1:1c4"),
+            ("1:1.0.post5", "1:1.post5"),
+            ("1.0+deadbeef", "1"),
+            ("1.0.dev6+deadbeef", "1.dev6"),
+            ("1.0a1+deadbeef", "1a1"),
+            ("1.0a1.post5+deadbeef", "1a1.post5"),
+            ("1.0a1.post5.dev6+deadbeef", "1a1.post5.dev6"),
+            ("1.0rc4+deadbeef", "1c4"),
+            ("1.0.post5+deadbeef", "1.post5"),
+            ("1:1.0+deadbeef", "1:1"),
+            ("1:1.0.dev6+deadbeef", "1:1.dev6"),
+            ("1:1.0a1+deadbeef", "1:1a1"),
+            ("1:1.0a1.post5+deadbeef", "1:1a1.post5"),
+            ("1:1.0a1.post5.dev6+deadbeef", "1:1a1.post5.dev6"),
+            ("1:1.0rc4+deadbeef", "1:1c4"),
+            ("1:1.0.post5+deadbeef", "1:1.post5"),
+        ],
+    )
+    def test_version_public(self, version, public):
+        assert Version(version).public == public
+
+    @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(
+        ("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
diff --git a/tox.ini b/tox.ini
index c543c3057..06bfed5c2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,6 +4,7 @@ envlist = py26,py27,pypy,py32,py33,py34,docs,pep8,py2pep8
 [testenv]
 deps =
     coverage
+    pretend
     pytest
 commands =
     coverage run --source=packaging/ -m pytest --capture=no --strict {posargs}

From 55c58bcf764ab747bd2d5b120bfca1e6bf7839c2 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Mon, 19 May 2014 12:01:10 -0400
Subject: [PATCH 03/17] Implement PEP 440 specifiers

---
 .coveragerc           |   3 +-
 docs/version.rst      |  50 ++++-
 packaging/_compat.py  |  27 +++
 packaging/version.py  | 271 +++++++++++++++++++++++++-
 tests/test_version.py | 432 +++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 776 insertions(+), 7 deletions(-)
 create mode 100644 packaging/_compat.py

diff --git a/.coveragerc b/.coveragerc
index 6b0817976..6e568301a 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,2 +1,3 @@
 [run]
-branch = True
\ No newline at end of file
+branch = True
+omit = packaging/_compat.py
diff --git a/docs/version.rst b/docs/version.rst
index 70dd0f8d9..eb08dba40 100644
--- a/docs/version.rst
+++ b/docs/version.rst
@@ -12,7 +12,7 @@ Usage
 
 .. doctest::
 
-    >>> from packaging.version import Version
+    >>> from packaging.version import Version, Specifier
     >>> v1 = Version("1.0a5")
     >>> v2 = Version("1.0")
     >>> v1
@@ -29,6 +29,28 @@ Usage
     Traceback (most recent call last):
         ...
     InvalidVersion: Invalid version: 'french toast'
+    >>> spec1 = Specifier("~=1.0")
+    >>> spec1
+    
+    >>> spec2 = Specifier(">=1.0")
+    >>> spec2
+    =1.0')>
+    >>> # We can combine specifiers
+    >>> combined_spec = spec1 & spec2
+    >>> combined_spec
+    =1.0,~=1.0')>
+    >>> # We can also implicitly combine a string specifier
+    >>> combined_spec &= "!=1.1"
+    >>> combined_spec
+    =1.0,~=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
 
 
 Reference
@@ -60,10 +82,34 @@ Reference
         represents a prerelease or a final release.
 
 
-.. class:: InvalidVersion:
+.. class:: Specifier(specifier)
+
+    This class abstracts handling of specifying the dependencies of a project.
+    It implements the scheme defined in `PEP 440`_. You can test membership
+    of a particular version within a set of specifiers in a :class:`Specifier`
+    instance by using the standard ``in`` operator (e.g.
+    ``Version("2.0") in Specifier("==2.0")``). You may also combine Specifier
+    instances using the ``&`` operator (``Specifier(">2") & Specifier(">3")``).
+
+    Both the membership test and the combination supports using raw strings
+    in place of already instantiated objects.
+
+    :param str specifier: The string representation of a specifier which will
+                          be parsed and normalized before use.
+    :raises InvalidSpecifier: If the ``specifier`` does not conform to PEP 440
+                              in any way then this exception will be raised.
+
+
+.. class:: InvalidVersion
 
     Raised when attempting to create a :class:`Version` with a version string
     that does not conform to `PEP 440`_.
 
 
+.. class:: 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/packaging/_compat.py b/packaging/_compat.py
new file mode 100644
index 000000000..f2ff38349
--- /dev/null
+++ b/packaging/_compat.py
@@ -0,0 +1,27 @@
+# Copyright 2014 Donald Stufft
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+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,
diff --git a/packaging/version.py b/packaging/version.py
index a8e529252..595ce3a2c 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -17,12 +17,11 @@
 import itertools
 import re
 
+from ._compat import string_types
 from ._structures import Infinity
 
-# TODO: We deviate from the spec in that we have no implicit specifier operator
-#       instead we mandate all specifiers must include an explicit operator.
 
-__all__ = ["Version"]
+__all__ = ["Version", "Specifier"]
 
 
 _Version = collections.namedtuple(
@@ -233,3 +232,269 @@ def _cmpkey(epoch, release, pre, post, dev, local):
         )
 
     return (epoch, release, pre, post, dev, local)
+
+
+class InvalidSpecifier(ValueError):
+    """
+    An invalid specifier was found, users should refer to PEP 440.
+    """
+
+
+class Specifier(object):
+
+    _regex = re.compile(
+        r"""
+        ^
+        (?P(~=|==|!=|<=|>=|<|>))
+        (?P
+            (?:
+                # 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
+
+                (?:[0-9]+:)?          # epoch
+                [0-9]+(?:\.[0-9]+)*   # release
+                (?:(a|b|c|rc)[0-9]+)? # pre release
+                (?:\.post[0-9]+)?     # post release
+
+                # 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_\.+]*[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
+
+                (?:[0-9]+:)?          # epoch
+                [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
+                (?:(a|b|c|rc)[0-9]+)? # pre release
+                (?:\.post[0-9]+)?     # post release
+                (?:\.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.
+                (?=": "greater_than_equal",
+        "<": "less_than",
+        ">": "greater_than",
+    }
+
+    def __init__(self, specs, prereleases=False):
+        # Normalize the specification to remove all of the whitespace
+        specs = specs.replace(" ", "")
+
+        # Split on comma to get each individual specification
+        _specs = set()
+        for spec in specs.split(","):
+            match = self._regex.search(spec)
+            if not match:
+                raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec))
+
+            _specs.add(
+                (match.group("operator"), match.group("version"))
+            )
+
+        # Set a frozen set for our specifications
+        self._specs = frozenset(_specs)
+
+    def __repr__(self):
+        return "".format(repr(str(self)))
+
+    def __str__(self):
+        return ",".join(["".join(s) for s in sorted(self._specs)])
+
+    def __hash__(self):
+        return hash(self._specs)
+
+    def __and__(self, other):
+        if isinstance(other, string_types):
+            other = Specifier(other)
+        elif not isinstance(other, Specifier):
+            return NotImplemented
+
+        return self.__class__(",".join([str(self), str(other)]))
+
+    def __eq__(self, other):
+        if isinstance(other, string_types):
+            other = Specifier(other)
+        elif not isinstance(other, Specifier):
+            return NotImplemented
+
+        return self._specs == other._specs
+
+    def __ne__(self, other):
+        if isinstance(other, string_types):
+            other = Specifier(other)
+        elif not isinstance(other, Specifier):
+            return NotImplemented
+
+        return self._specs != other._specs
+
+    def __contains__(self, item):
+        # Normalize item to a Version, this allows us to have a shortcut for
+        # ``"2.0" in Specifier(">=2")
+        if not isinstance(item, Version):
+            item = Version(item)
+
+        # Ensure that the passed in version matches all of our version
+        # specifiers
+        return all(
+            self._get_operator(op)(item, spec) for op, spec, in self._specs
+        )
+
+    def _get_operator(self, op):
+        return getattr(self, "_compare_{0}".format(self._operators[op]))
+
+    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))
+
+    def _compare_equal(self, prospective, spec):
+        # We need special logic to handle prefix matching
+        if spec.endswith(".*"):
+            # 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
+
+    def _compare_not_equal(self, prospective, spec):
+        return not self._compare_equal(prospective, spec)
+
+    def _compare_less_than_equal(self, prospective, spec):
+        return prospective <= Version(spec)
+
+    def _compare_greater_than_equal(self, prospective, spec):
+        return prospective >= Version(spec)
+
+    def _compare_less_than(self, prospective, spec):
+        # Less than are defined as exclusive operators, this implies that
+        # pre-releases do not match for the same series as the spec. This is
+        # implemented by making V imply !=V.*.
+        return (prospective > Version(spec)
+                and self._get_operator("!=")(prospective, spec + ".*"))
+
+
+_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):])
+    right_split.append(left[len(right_split):])
+
+    # 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)),
+    )
diff --git a/tests/test_version.py b/tests/test_version.py
index 3d1236342..f021eb68e 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -15,11 +15,14 @@
 
 import itertools
 import operator
+import re
 
 import pretend
 import pytest
 
-from packaging.version import Version, InvalidVersion
+from packaging.version import (
+    Version, InvalidVersion, Specifier, InvalidSpecifier,
+)
 
 
 # This list must be in the correct sorting order
@@ -329,3 +332,430 @@ def test_compare_other(self, op, expected):
         )
 
         assert getattr(operator, op)(Version("1"), other) is expected
+
+
+# These should all be without spaces, we'll generate some with spaces using
+# these as templates.
+SPECIFIERS = [
+    "~=2.0", "==2.1.*", "==2.1.0.3", "!=2.2.*", "!=2.2.0.5", "<=5", ">=7.9a1",
+    "<1.0.dev1", ">2.0.post1",
+]
+
+
+class TestSpecifier:
+
+    @pytest.mark.parametrize(
+        "specifier",
+        # Generate all possible combinations of the SPECIFIERS to test to make
+        # sure they all work.
+        [
+            ",".join(combination)
+            for combination in itertools.chain(*(
+                itertools.combinations(SPECIFIERS, n)
+                for n in range(1, len(SPECIFIERS) + 1)
+            ))
+        ]
+        +
+        # Do the same thing, except include spaces in the specifiers
+        [
+            ",".join([
+                " ".join(re.split(r"(~=|==|!=|<=|>=|<|>)", item)[1:])
+                for item in combination
+            ])
+            for combination in itertools.chain(*(
+                itertools.combinations(SPECIFIERS, n)
+                for n in range(1, len(SPECIFIERS) + 1)
+            ))
+        ]
+        +
+        # Finally do the same thing once more, except join some with spaces and
+        # some without.
+        [
+            ",".join([
+                ("" if j % 2 else " ").join(
+                    re.split(r"(~=|==|!=|<=|>=|<|>)", item)[1:]
+                )
+                for j, item in enumerate(combination)
+            ])
+            for combination in itertools.chain(*(
+                itertools.combinations(SPECIFIERS, n)
+                for n in range(1, len(SPECIFIERS) + 1)
+            ))
+        ]
+    )
+    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(
+        ("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"),
+
+            # Multiple item specifiers should be sorted lexicographically
+            ("<2,!=1.5", "!=1.5,<2"),
+            (
+                "~=1.3.5,>5.3,==1.3.*,<=700,>=0,!=99.99,<1000",
+                "!=99.99,<1000,<=700,==1.3.*,>5.3,>=0,~=1.3.5",
+            ),
+
+            # Spaces should be removed
+            ("== 2.0", "==2.0"),
+            (">=2.0, !=2.1.0", "!=2.1.0,>=2.0"),
+            ("< 2, >= 5,~= 2.2,==5.4", "<2,==5.4,>=5,~=2.2"),
+        ],
+    )
+    def test_specifiers_str_and_repr(self, specifier, expected):
+        spec = Specifier(specifier)
+
+        assert str(spec) == expected
+        assert repr(spec) == "".format(repr(expected))
+
+    @pytest.mark.parametrize("specifier", SPECIFIERS)
+    def test_specifiers_hash(self, specifier):
+        assert hash(Specifier(specifier)) == hash(Specifier(specifier))
+
+    @pytest.mark.parametrize(
+        "specifiers",
+        [
+            ["!=2", "==2.*"],
+            [">=5.7", "<7000"],
+            ["==2.5.0+3", ">1"],
+        ],
+    )
+    def test_combining_specifiers(self, specifiers):
+        # Test combining Specifier objects
+        spec = Specifier(specifiers[0])
+        for s in specifiers[1:]:
+            spec &= Specifier(s)
+        assert spec == Specifier(",".join(specifiers))
+
+        # Test combining a string with a Specifier object
+        spec = Specifier(specifiers[0])
+        for s in specifiers[1:]:
+            spec &= s
+        assert spec == Specifier(",".join(specifiers))
+
+    def test_combining_non_specifiers(self):
+        with pytest.raises(TypeError):
+            Specifier("==2.0") & 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(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
+
+    @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.*"),
+
+                # 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"),
+
+                # Test the less than operation
+                ("1", "<2"),
+                ("2.0", "<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"),
+            ]
+        ]
+        +
+        [
+            (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.1", ">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)
+
+        if expected:
+            # Test that the plain string form works
+            assert version in spec
+
+            # Test that the version instance form works
+            assert Version(version) in spec
+        else:
+            # Test that the plain string form works
+            assert version not in spec
+
+            # Test that the version instance form works
+            assert Version(version) not in spec

From edb18dd900f78c8c97bd0d792ef8e98088b6d20e Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Wed, 21 May 2014 21:45:26 -0400
Subject: [PATCH 04/17] Create an invoke task to check PEP440 compliance with
 PyPI

---
 tasks/__init__.py      |  20 ++++++
 tasks/check.py         | 153 +++++++++++++++++++++++++++++++++++++++++
 tasks/paths.py         |  20 ++++++
 tasks/requirements.txt |   3 +
 tox.ini                |   2 +-
 5 files changed, 197 insertions(+), 1 deletion(-)
 create mode 100644 tasks/__init__.py
 create mode 100644 tasks/check.py
 create mode 100644 tasks/paths.py
 create mode 100644 tasks/requirements.txt

diff --git a/tasks/__init__.py b/tasks/__init__.py
new file mode 100644
index 000000000..6d67b6834
--- /dev/null
+++ b/tasks/__init__.py
@@ -0,0 +1,20 @@
+# Copyright 2014 Donald Stufft
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import absolute_import, division, print_function
+
+import invoke
+
+from . import check
+
+ns = invoke.Collection(check)
diff --git a/tasks/check.py b/tasks/check.py
new file mode 100644
index 000000000..f3ebaf6fa
--- /dev/null
+++ b/tasks/check.py
@@ -0,0 +1,153 @@
+# Copyright 2014 Donald Stufft
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import itertools
+import os.path
+
+try:
+    import xmlrpc.client as xmlrpc_client
+except ImportError:
+    import xmlrpclib as xmlrpc_client
+
+import invoke
+import pkg_resources
+import progress.bar
+
+from packaging.version import Version
+
+from .paths import CACHE
+
+
+def _parse_version(value):
+    try:
+        return Version(value)
+    except ValueError:
+        return None
+
+
+@invoke.task
+def pep440(cached=False):
+    cache_path = os.path.join(CACHE, "pep440.json")
+
+    # If we were given --cached, then we want to attempt to use cached data if
+    # possible
+    if cached:
+        try:
+            with open(cache_path, "r") as fp:
+                data = json.load(fp)
+        except Exception:
+            data = None
+    else:
+        data = None
+
+    # If we don't have data, then let's go fetch it from PyPI
+    if data is None:
+        bar = progress.bar.ShadyBar("Fetching Versions")
+        client = xmlrpc_client.Server("https://pypi.python.org/pypi")
+
+        data = {
+            project: client.package_releases(project, True)
+            for project in bar.iter(client.list_packages())
+        }
+
+        os.makedirs(os.path.dirname(cache_path), exist_ok=True)
+        with open(cache_path, "w") as fp:
+            json.dump(data, fp)
+
+    # Get a list of all of the version numbers on PyPI
+    all_versions = list(itertools.chain.from_iterable(data.values()))
+
+    # Determine the total number of versions which are compatible with the
+    # current routine
+    parsed_versions = [
+        _parse_version(v)
+        for v in all_versions
+        if _parse_version(v) is not None
+    ]
+
+    # Determine a list of projects that sort exactly the same between
+    # pkg_resources and PEP 440
+    compatible_sorting = [
+        project for project, versions in data.items()
+        if (sorted(versions, key=pkg_resources.parse_version)
+            == sorted((x for x in versions if _parse_version(x)), key=Version))
+    ]
+
+    # Determine a list of projects that sort exactly the same between
+    # pkg_resources and PEP 440 when invalid versions are filtered out
+    filtered_compatible_sorting = [
+        project
+        for project, versions in (
+            (p, [v for v in vs if _parse_version(v) is not None])
+            for p, vs in data.items()
+        )
+        if (sorted(versions, key=pkg_resources.parse_version)
+            == sorted(versions, key=Version))
+    ]
+
+    # Determine a list of projects which do not have any versions that are
+    # valid with PEP 440 and which have any versions registered
+    only_invalid_versions = [
+        project for project, versions in data.items()
+        if (versions
+            and not [v for v in versions if _parse_version(v) is not None])
+    ]
+
+    # Determine a list of projects which have matching latest versions between
+    # pkg_resources and PEP 440
+    differing_latest_versions = [
+        project for project, versions in data.items()
+        if (sorted(versions, key=pkg_resources.parse_version)[-1:]
+            != sorted(
+                (x for x in versions if _parse_version(x)),
+                key=Version)[-1:])
+    ]
+
+    # Print out our findings
+    print(
+        "Total Version Compatibility:              {}/{} ({:.2%})".format(
+            len(parsed_versions),
+            len(all_versions),
+            len(parsed_versions) / len(all_versions),
+        )
+    )
+    print(
+        "Total Sorting Compatibility (Unfiltered): {}/{} ({:.2%})".format(
+            len(compatible_sorting),
+            len(data),
+            len(compatible_sorting) / len(data),
+        )
+    )
+    print(
+        "Total Sorting Compatibility (Filtered):   {}/{} ({:.2%})".format(
+            len(filtered_compatible_sorting),
+            len(data),
+            len(filtered_compatible_sorting) / len(data),
+        )
+    )
+    print(
+        "Projects with No Compatible Versions:     {}/{} ({:.2%})".format(
+            len(only_invalid_versions),
+            len(data),
+            len(only_invalid_versions) / len(data),
+        )
+    )
+    print(
+        "Projects with Differing Latest Version:   {}/{} ({:.2%})".format(
+            len(differing_latest_versions),
+            len(data),
+            len(differing_latest_versions) / len(data),
+        )
+    )
diff --git a/tasks/paths.py b/tasks/paths.py
new file mode 100644
index 000000000..02f190053
--- /dev/null
+++ b/tasks/paths.py
@@ -0,0 +1,20 @@
+# Copyright 2014 Donald Stufft
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+
+
+PROJECT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
+
+CACHE = os.path.join(PROJECT, ".cache")
diff --git a/tasks/requirements.txt b/tasks/requirements.txt
new file mode 100644
index 000000000..5677c0e89
--- /dev/null
+++ b/tasks/requirements.txt
@@ -0,0 +1,3 @@
+# The requirements required to invoke the tasks
+invoke
+progress
diff --git a/tox.ini b/tox.ini
index 06bfed5c2..c1b658831 100644
--- a/tox.ini
+++ b/tox.ini
@@ -33,7 +33,7 @@ basepython = python2.6
 deps =
     flake8
     pep8-naming
-commands = flake8 .
+commands = flake8 . --exclude tasks/*,.tox,*.egg
 
 [flake8]
 exclude = .tox,*.egg

From c96bae148fd4cfc609a8b6894fce7b69220855ea Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Sun, 22 Jun 2014 13:17:02 -0400
Subject: [PATCH 05/17] Allow alternative syntax for backwards compatability

---
 packaging/version.py  |  57 +++++++-----
 tests/test_version.py | 211 +++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 244 insertions(+), 24 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index 595ce3a2c..44c1a31c8 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -42,19 +42,24 @@ class Version(object):
         r"""
         ^
         (?:
-            (?:(?P[0-9]+):)?          # epoch
-            (?P[0-9]+(?:\.[0-9]+)*) # release segment
-            (?P
                         # pre release
-                (?P(a|b|c|rc))        #  - pre-release letter
-                (?P[0-9]+)            #  - pre-release number
+            (?:(?P[0-9]+):)?               # epoch
+            (?P[0-9]+(?:\.[0-9]+)*)      # release segment
+            (?P
                              # pre release
+                (?:[-\.])?
+                (?P(a|b|c|rc|alpha|beta))  #  - pre-release letter
+                (?P[0-9]+)?                #  - pre-release number
+            )?
+            (?:\.post(?P[0-9]+))?           # post release
+            (?P                              # dev release
+                [-\.]?
+                (?Pdev)
+                (?P[0-9]+)?
             )?
-            (?:\.post(?P[0-9]+))?      # post release
-            (?:\.dev(?P[0-9]+))?        # dev release
         )
         (?:\+(?P[a-z0-9]+(?:[a-z0-9\.]*[a-z0-9])?))? # local version
         $
         """,
-        re.VERBOSE,
+        re.VERBOSE | re.IGNORECASE,
     )
 
     def __init__(self, version):
@@ -66,10 +71,10 @@ def __init__(self, version):
         # Store the parsed out pieces of the version
         self._version = _Version(
             epoch=int(match.group("epoch")) if match.group("epoch") else 0,
-            release=_parse_dot_version(match.group("release")),
+            release=_parse_release_version(match.group("release")),
             pre=_parse_pre_version(match.group("pre_l"), match.group("pre_n")),
             post=int(match.group("post")) if match.group("post") else None,
-            dev=int(match.group("dev")) if match.group("dev") else None,
+            dev=_parse_pre_version(match.group("dev_l"), match.group("dev_n")),
             local=_parse_local_version(match.group("local")),
         )
 
@@ -106,7 +111,7 @@ def __str__(self):
 
         # Development release
         if self._version.dev is not None:
-            parts.append(".dev{0}".format(self._version.dev))
+            parts.append(".dev{0}".format(self._version.dev[1]))
 
         # Local version segment
         if self._version.local is not None:
@@ -158,7 +163,7 @@ def is_prerelease(self):
         return bool(self._version.dev or self._version.pre)
 
 
-def _parse_dot_version(part):
+def _parse_release_version(part):
     """
     Takes a string like "1.0.4.0" and turns it into (1, 0, 4).
     """
@@ -175,13 +180,19 @@ def _parse_dot_version(part):
 
 
 def _parse_pre_version(letter, number):
-    if letter and 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 consider the "rc" form of a pre-release to be long-form for the
         # "c" form, thus we normalize "rc" to "c" so we can properly compare
         # them as equal.
         if letter == "rc":
             letter = "c"
-        return (letter, int(number))
+
+        return letter, int(number)
 
 
 def _parse_local_version(local):
@@ -231,7 +242,7 @@ def _cmpkey(epoch, release, pre, post, dev, local):
             for i in local
         )
 
-    return (epoch, release, pre, post, dev, local)
+    return epoch, release, pre, post, dev, local
 
 
 class InvalidSpecifier(ValueError):
@@ -255,14 +266,14 @@ class Specifier(object):
 
                 (?:[0-9]+:)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
-                (?:(a|b|c|rc)[0-9]+)? # pre release
+                (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
                 (?:\.post[0-9]+)?     # post release
 
                 # 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_\.+]*[a-z0-9])?) # local
+                    (?:[-\.]?dev[0-9]*)?                       # dev release
+                    (?:\+[a-z0-9]+(?:[a-z0-9_\.+]*[a-z0-9])?)? # local
                     |
                     \.\*  # Wild card syntax of .*
                 )?
@@ -275,9 +286,9 @@ class Specifier(object):
 
                 (?:[0-9]+:)?          # epoch
                 [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
-                (?:(a|b|c|rc)[0-9]+)? # pre release
+                (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
                 (?:\.post[0-9]+)?     # post release
-                (?:\.dev[0-9]+)?      # dev release
+                (?:[-\.]?dev[0-9]*)?  # dev release
             )
             |
             (?:
@@ -291,14 +302,14 @@ class Specifier(object):
 
                 (?:[0-9]+:)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
-                (?:(a|b|c|rc)[0-9]+)? # pre release
+                (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
                 (?:\.post[0-9]+)?     # post release
-                (?:\.dev[0-9]+)?      # dev release
+                (?:[-\.]?dev[0-9]*)?  # dev release
             )
         )
         $
         """,
-        re.VERBOSE,
+        re.VERBOSE | re.IGNORECASE,
     )
 
     _operators = {
diff --git a/tests/test_version.py b/tests/test_version.py
index f021eb68e..13c60b074 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -57,7 +57,6 @@ def test_valid_versions(self, version):
             "french toast",
 
             # Versions with invalid local versions
-            "1.0+A",
             "1.0+a+",
             "1.0++",
             "1.0+_foobar",
@@ -70,6 +69,108 @@ 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.0c0"),
+            ("1.0.c", "1.0c0"),
+            ("1.0.c1", "1.0c1"),
+            ("1.0-c", "1.0c0"),
+            ("1.0-c1", "1.0c1"),
+            ("1.0rc", "1.0c0"),
+            ("1.0.rc", "1.0c0"),
+            ("1.0.rc1", "1.0c1"),
+            ("1.0-rc", "1.0c0"),
+            ("1.0-rc1", "1.0c1"),
+            ("1.0C", "1.0c0"),
+            ("1.0.C", "1.0c0"),
+            ("1.0.C1", "1.0c1"),
+            ("1.0-C", "1.0c0"),
+            ("1.0-C1", "1.0c1"),
+            ("1.0RC", "1.0c0"),
+            ("1.0.RC", "1.0c0"),
+            ("1.0.RC1", "1.0c1"),
+            ("1.0-RC", "1.0c0"),
+            ("1.0-RC1", "1.0c1"),
+
+            # Local version case insensitivity
+            ("1.0+AbC", "1.0+abc"),
+
+            # Integer Normalization
+            ("1.01", "1.1"),
+            ("1.0a05", "1a5"),
+            ("1.0b07", "1a7"),
+            ("1.0c056", "1a56"),
+            ("1.0rc09", "1a9"),
+            ("1.0.post000", "1.post0"),
+            ("1.1.dev09000", "1.1.dev9000"),
+            ("00:1.2", "1.2"),
+            ("0100:0.0", "100:0"),
+        ],
+    )
+    def test_normalized_versions(self, version, normalized):
+        str(Version(version)) == normalized
+
     @pytest.mark.parametrize(
         ("version", "expected"),
         [
@@ -436,6 +537,114 @@ 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",
+
+            # 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",
+        ],
+    )
+    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"),
         [

From 8fc910b9b2564bd1282767ee847fb44b36972411 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Sun, 22 Jun 2014 15:04:51 -0400
Subject: [PATCH 06/17] Allow identity comparisons, even for non PEP 440
 compliant versions

---
 packaging/version.py  | 39 ++++++++++++++++++++++++++++++++++++---
 tests/test_version.py | 28 +++++++++++++++++++++++++++-
 2 files changed, 63 insertions(+), 4 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index 44c1a31c8..8e14f38c5 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -256,8 +256,19 @@ class Specifier(object):
     _regex = re.compile(
         r"""
         ^
-        (?P(~=|==|!=|<=|>=|<|>))
+        (?P(~=|==|!=|<=|>=|<|>|===))
         (?P
+            (?:
+                # 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
+                .*        # We just match everything, 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
@@ -320,6 +331,7 @@ class Specifier(object):
         ">=": "greater_than_equal",
         "<": "less_than",
         ">": "greater_than",
+        "===": "identity",
     }
 
     def __init__(self, specs, prereleases=False):
@@ -376,13 +388,31 @@ def __ne__(self, other):
     def __contains__(self, item):
         # Normalize item to a Version, this allows us to have a shortcut for
         # ``"2.0" in Specifier(">=2")
+        version_item = item
         if not isinstance(item, Version):
-            item = Version(item)
+            try:
+                version_item = Version(item)
+            except ValueError:
+                # If we cannot parse this as a version, then we can only
+                # support identity comparison so do a quick check to see if the
+                # spec contains any non identity specifiers
+                #
+                # This will return False if we do not have any specifiers, this
+                # is on purpose as a non PEP 440 version should require
+                # explicit opt in because otherwise they cannot be sanely
+                # prioritized
+                if (not self._specs
+                        or any(op != "===" for op, _ in self._specs)):
+                    return False
 
         # Ensure that the passed in version matches all of our version
         # specifiers
         return all(
-            self._get_operator(op)(item, spec) for op, spec, in self._specs
+            self._get_operator(op)(
+                version_item if op != "===" else item,
+                spec,
+            )
+            for op, spec, in self._specs
         )
 
     def _get_operator(self, op):
@@ -469,6 +499,9 @@ def _compare_greater_than(self, prospective, spec):
         return (prospective > Version(spec)
                 and self._get_operator("!=")(prospective, spec + ".*"))
 
+    def _compare_identity(self, prospective, spec):
+        return prospective.lower() == spec.lower()
+
 
 _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
 
diff --git a/tests/test_version.py b/tests/test_version.py
index 13c60b074..b94e6d93e 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -439,7 +439,7 @@ def test_compare_other(self, op, expected):
 # these as templates.
 SPECIFIERS = [
     "~=2.0", "==2.1.*", "==2.1.0.3", "!=2.2.*", "!=2.2.0.5", "<=5", ">=7.9a1",
-    "<1.0.dev1", ">2.0.post1",
+    "<1.0.dev1", ">2.0.post1", "===lolwat",
 ]
 
 
@@ -968,3 +968,29 @@ def test_specifiers(self, version, spec, expected):
 
             # Test that the version instance form works
             assert Version(version) not in spec
+
+    @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),
+
+            # Test multiple specs combined with an identity comparison
+            ("nope", "===nope,!=1.0", False),
+            ("1.0.0", "===1.0.0,==1.*", True),
+            ("1.0.0", "===1.0,==1.*", False),
+        ],
+    )
+    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

From 77eee20e51b2440a8b191a02438b6623f9156260 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Sun, 22 Jun 2014 18:02:51 -0400
Subject: [PATCH 07/17] Normalize post releases like pre-releases and
 development releases

---
 packaging/version.py  | 33 +++++++++++++++++++++++----------
 tests/test_version.py | 30 ++++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index 8e14f38c5..e88ceb5c7 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -45,11 +45,15 @@ class Version(object):
             (?:(?P[0-9]+):)?               # epoch
             (?P[0-9]+(?:\.[0-9]+)*)      # release segment
             (?P
                              # pre release
-                (?:[-\.])?
+                [-\.]?
                 (?P(a|b|c|rc|alpha|beta))  #  - pre-release letter
                 (?P[0-9]+)?                #  - pre-release number
             )?
-            (?:\.post(?P[0-9]+))?           # post release
+            (?P                             # post release
+                [-\.]?
+                (?Ppost)
+                (?P[0-9]+)?
+            )?
             (?P                              # dev release
                 [-\.]?
                 (?Pdev)
@@ -72,9 +76,18 @@ def __init__(self, version):
         self._version = _Version(
             epoch=int(match.group("epoch")) if match.group("epoch") else 0,
             release=_parse_release_version(match.group("release")),
-            pre=_parse_pre_version(match.group("pre_l"), match.group("pre_n")),
-            post=int(match.group("post")) if match.group("post") else None,
-            dev=_parse_pre_version(match.group("dev_l"), match.group("dev_n")),
+            pre=_parse_letter_version(
+                match.group("pre_l"),
+                match.group("pre_n"),
+            ),
+            post=_parse_letter_version(
+                match.group("post_l"),
+                match.group("post_n"),
+            ),
+            dev=_parse_letter_version(
+                match.group("dev_l"),
+                match.group("dev_n"),
+            ),
             local=_parse_local_version(match.group("local")),
         )
 
@@ -107,7 +120,7 @@ def __str__(self):
 
         # Post-release
         if self._version.post is not None:
-            parts.append(".post{0}".format(self._version.post))
+            parts.append(".post{0}".format(self._version.post[1]))
 
         # Development release
         if self._version.dev is not None:
@@ -179,7 +192,7 @@ def _parse_release_version(part):
     )
 
 
-def _parse_pre_version(letter, number):
+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.
@@ -278,7 +291,7 @@ class Specifier(object):
                 (?:[0-9]+:)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
                 (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
-                (?:\.post[0-9]+)?     # post release
+                (?:[-\.]?post[0-9]*)? # post release
 
                 # You cannot use a wild card and a dev or local version
                 # together so group them with a | and make them optional.
@@ -298,7 +311,7 @@ class Specifier(object):
                 (?:[0-9]+:)?          # epoch
                 [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
                 (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
-                (?:\.post[0-9]+)?     # post release
+                (?:[-\.]?post[0-9]*)? # post release
                 (?:[-\.]?dev[0-9]*)?  # dev release
             )
             |
@@ -314,7 +327,7 @@ class Specifier(object):
                 (?:[0-9]+:)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
                 (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
-                (?:\.post[0-9]+)?     # post release
+                (?:[-\.]?post[0-9]*)? # post release
                 (?:[-\.]?dev[0-9]*)?  # dev release
             )
         )
diff --git a/tests/test_version.py b/tests/test_version.py
index b94e6d93e..a33f5b172 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -153,6 +153,21 @@ def test_invalid_versions(self, version):
             ("1.0-RC", "1.0c0"),
             ("1.0-RC1", "1.0c1"),
 
+            # 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.0.POST1", "1.0.post1"),
+            ("1.0-POST", "1.0.post0"),
+            ("1.0-POST1", "1.0.post1"),
+
             # Local version case insensitivity
             ("1.0+AbC", "1.0+abc"),
 
@@ -621,6 +636,21 @@ def test_specifiers_invalid(self, specifier):
             "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",
+
             # Local version case insensitivity
             "1.0+AbC"
 

From 4b016f0088b201889d7fdff37fff4370075dce7c Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Sun, 22 Jun 2014 18:12:52 -0400
Subject: [PATCH 08/17] Do not throw away trailing digits for representation

Instead of normalizing 1.0 to 1, we'll continue to keep all of the
digits that a version normally contains so we can more faithfully
recreate it. The compare key will still continue to drop trailing
zeros for comparing versions.
---
 docs/version.rst      |   4 +-
 packaging/version.py  |  32 ++++++------
 tests/test_version.py | 112 +++++++++++++++++++++---------------------
 3 files changed, 73 insertions(+), 75 deletions(-)

diff --git a/docs/version.rst b/docs/version.rst
index eb08dba40..74e40bd30 100644
--- a/docs/version.rst
+++ b/docs/version.rst
@@ -16,9 +16,9 @@ Usage
     >>> v1 = Version("1.0a5")
     >>> v2 = Version("1.0")
     >>> v1
-    
+    
     >>> v2
-    
+    
     >>> v1 < v2
     True
     >>> v1.is_prerelease
diff --git a/packaging/version.py b/packaging/version.py
index e88ceb5c7..bf8935a7c 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -75,7 +75,7 @@ def __init__(self, version):
         # Store the parsed out pieces of the version
         self._version = _Version(
             epoch=int(match.group("epoch")) if match.group("epoch") else 0,
-            release=_parse_release_version(match.group("release")),
+            release=tuple(int(i) for i in match.group("release").split(".")),
             pre=_parse_letter_version(
                 match.group("pre_l"),
                 match.group("pre_n"),
@@ -176,22 +176,6 @@ def is_prerelease(self):
         return bool(self._version.dev or self._version.pre)
 
 
-def _parse_release_version(part):
-    """
-    Takes a string like "1.0.4.0" and turns it into (1, 0, 4).
-    """
-    return tuple(
-        reversed(
-            list(
-                itertools.dropwhile(
-                    lambda x: x == 0,
-                    reversed(list(int(i) for i in part.split("."))),
-                )
-            )
-        )
-    )
-
-
 def _parse_letter_version(letter, number):
     if letter:
         # We consider there to be an implicit 0 in a pre-release if there is
@@ -220,6 +204,20 @@ def _parse_local_version(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
diff --git a/tests/test_version.py b/tests/test_version.py
index a33f5b172..8fbc5d544 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -189,20 +189,20 @@ def test_normalized_versions(self, version, normalized):
     @pytest.mark.parametrize(
         ("version", "expected"),
         [
-            ("1.0.dev456", "1.dev456"),
-            ("1.0a1", "1a1"),
-            ("1.0a2.dev456", "1a2.dev456"),
-            ("1.0a12.dev456", "1a12.dev456"),
-            ("1.0a12", "1a12"),
-            ("1.0b1.dev456", "1b1.dev456"),
-            ("1.0b2", "1b2"),
-            ("1.0b2.post345.dev456", "1b2.post345.dev456"),
-            ("1.0b2.post345", "1b2.post345"),
-            ("1.0c1.dev456", "1c1.dev456"),
-            ("1.0c1", "1c1"),
-            ("1.0", "1"),
-            ("1.0.post456.dev34", "1.post456.dev34"),
-            ("1.0.post456", "1.post456"),
+            ("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.0c1.dev456", "1.0c1.dev456"),
+            ("1.0c1", "1.0c1"),
+            ("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"),
@@ -216,20 +216,20 @@ def test_normalized_versions(self, version, normalized):
             ("1.2+abc123", "1.2+abc123"),
             ("1.2+abc123def", "1.2+abc123def"),
             ("1.1.dev1", "1.1.dev1"),
-            ("7:1.0.dev456", "7:1.dev456"),
-            ("7:1.0a1", "7:1a1"),
-            ("7:1.0a2.dev456", "7:1a2.dev456"),
-            ("7:1.0a12.dev456", "7:1a12.dev456"),
-            ("7:1.0a12", "7:1a12"),
-            ("7:1.0b1.dev456", "7:1b1.dev456"),
-            ("7:1.0b2", "7:1b2"),
-            ("7:1.0b2.post345.dev456", "7:1b2.post345.dev456"),
-            ("7:1.0b2.post345", "7:1b2.post345"),
-            ("7:1.0c1.dev456", "7:1c1.dev456"),
-            ("7:1.0c1", "7:1c1"),
-            ("7:1.0", "7:1"),
-            ("7:1.0.post456.dev34", "7:1.post456.dev34"),
-            ("7:1.0.post456", "7:1.post456"),
+            ("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.0c1.dev456", "7:1.0c1.dev456"),
+            ("7:1.0c1", "7:1.0c1"),
+            ("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"),
@@ -253,34 +253,34 @@ def test_version_hash(self, version):
     @pytest.mark.parametrize(
         ("version", "public"),
         [
-            ("1.0", "1"),
-            ("1.0.dev6", "1.dev6"),
-            ("1.0a1", "1a1"),
-            ("1.0a1.post5", "1a1.post5"),
-            ("1.0a1.post5.dev6", "1a1.post5.dev6"),
-            ("1.0rc4", "1c4"),
-            ("1.0.post5", "1.post5"),
-            ("1:1.0", "1:1"),
-            ("1:1.0.dev6", "1:1.dev6"),
-            ("1:1.0a1", "1:1a1"),
-            ("1:1.0a1.post5", "1:1a1.post5"),
-            ("1:1.0a1.post5.dev6", "1:1a1.post5.dev6"),
-            ("1:1.0rc4", "1:1c4"),
-            ("1:1.0.post5", "1:1.post5"),
-            ("1.0+deadbeef", "1"),
-            ("1.0.dev6+deadbeef", "1.dev6"),
-            ("1.0a1+deadbeef", "1a1"),
-            ("1.0a1.post5+deadbeef", "1a1.post5"),
-            ("1.0a1.post5.dev6+deadbeef", "1a1.post5.dev6"),
-            ("1.0rc4+deadbeef", "1c4"),
-            ("1.0.post5+deadbeef", "1.post5"),
-            ("1:1.0+deadbeef", "1:1"),
-            ("1:1.0.dev6+deadbeef", "1:1.dev6"),
-            ("1:1.0a1+deadbeef", "1:1a1"),
-            ("1:1.0a1.post5+deadbeef", "1:1a1.post5"),
-            ("1:1.0a1.post5.dev6+deadbeef", "1:1a1.post5.dev6"),
-            ("1:1.0rc4+deadbeef", "1:1c4"),
-            ("1:1.0.post5+deadbeef", "1:1.post5"),
+            ("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.0c4"),
+            ("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.0c4"),
+            ("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.0c4"),
+            ("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.0c4"),
+            ("1:1.0.post5+deadbeef", "1:1.0.post5"),
         ],
     )
     def test_version_public(self, version, public):

From 314effac7413d08926fff93f1570897a55dd07f5 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Sun, 22 Jun 2014 18:40:55 -0400
Subject: [PATCH 09/17] Fix the normalization of letter versions and local
 versions

---
 packaging/version.py  | 17 ++++++++++++-----
 tests/test_version.py | 14 +++++++-------
 2 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index bf8935a7c..3602b2123 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -183,10 +183,17 @@ def _parse_letter_version(letter, number):
         if number is None:
             number = 0
 
-        # We consider the "rc" form of a pre-release to be long-form for the
-        # "c" form, thus we normalize "rc" to "c" so we can properly compare
-        # them as equal.
-        if letter == "rc":
+        # 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 == "rc":
             letter = "c"
 
         return letter, int(number)
@@ -198,7 +205,7 @@ def _parse_local_version(local):
     """
     if local is not None:
         return tuple(
-            part if not part.isdigit() else int(part)
+            part.lower() if not part.isdigit() else int(part)
             for part in local.split(".")
         )
 
diff --git a/tests/test_version.py b/tests/test_version.py
index 8fbc5d544..296f9a0a1 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -173,18 +173,18 @@ def test_invalid_versions(self, version):
 
             # Integer Normalization
             ("1.01", "1.1"),
-            ("1.0a05", "1a5"),
-            ("1.0b07", "1a7"),
-            ("1.0c056", "1a56"),
-            ("1.0rc09", "1a9"),
-            ("1.0.post000", "1.post0"),
+            ("1.0a05", "1.0a5"),
+            ("1.0b07", "1.0b7"),
+            ("1.0c056", "1.0c56"),
+            ("1.0rc09", "1.0c9"),
+            ("1.0.post000", "1.0.post0"),
             ("1.1.dev09000", "1.1.dev9000"),
             ("00:1.2", "1.2"),
-            ("0100:0.0", "100:0"),
+            ("0100:0.0", "100:0.0"),
         ],
     )
     def test_normalized_versions(self, version, normalized):
-        str(Version(version)) == normalized
+        assert str(Version(version)) == normalized
 
     @pytest.mark.parametrize(
         ("version", "expected"),

From 6e3268aa634c49e9a2c53779829e0911d3c95811 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Sun, 22 Jun 2014 20:46:09 -0400
Subject: [PATCH 10/17] Switch from : to ! to denote an Epoch

The : character has special connotations on Windows, which makes it
an invalid character, so we'll use ! instead which is not an invalid
character.
---
 packaging/version.py  |  10 +--
 tests/test_version.py | 152 +++++++++++++++++++++---------------------
 2 files changed, 81 insertions(+), 81 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index 3602b2123..2b37f62aa 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -42,7 +42,7 @@ class Version(object):
         r"""
         ^
         (?:
-            (?:(?P[0-9]+):)?               # epoch
+            (?:(?P[0-9]+)!)?               # epoch
             (?P[0-9]+(?:\.[0-9]+)*)      # release segment
             (?P
                              # pre release
                 [-\.]?
@@ -109,7 +109,7 @@ def __str__(self):
 
         # Epoch
         if self._version.epoch != 0:
-            parts.append("{0}:".format(self._version.epoch))
+            parts.append("{0}!".format(self._version.epoch))
 
         # Release segment
         parts.append(".".join(str(x) for x in self._version.release))
@@ -293,7 +293,7 @@ class Specifier(object):
                 # operators separately to enable that.
                 (?<===|!=)            # Only match for equals and not equals
 
-                (?:[0-9]+:)?          # epoch
+                (?:[0-9]+!)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
                 (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
                 (?:[-\.]?post[0-9]*)? # post release
@@ -313,7 +313,7 @@ class Specifier(object):
                 # release segment.
                 (?<=~=)               # Only match for the compatible operator
 
-                (?:[0-9]+:)?          # epoch
+                (?:[0-9]+!)?          # epoch
                 [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
                 (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
                 (?:[-\.]?post[0-9]*)? # post release
@@ -329,7 +329,7 @@ class Specifier(object):
                                       # operators so we want to make sure they
                                       # don't match here.
 
-                (?:[0-9]+:)?          # epoch
+                (?:[0-9]+!)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
                 (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
                 (?:[-\.]?post[0-9]*)? # post release
diff --git a/tests/test_version.py b/tests/test_version.py
index 296f9a0a1..34e7f3d7b 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -35,12 +35,12 @@
     "1.2+abc123", "1.2+abc123def", "1.2+1234.abc", "1.2+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.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.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.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",
 ]
 
 
@@ -179,8 +179,8 @@ def test_invalid_versions(self, version):
             ("1.0rc09", "1.0c9"),
             ("1.0.post000", "1.0.post0"),
             ("1.1.dev09000", "1.1.dev9000"),
-            ("00:1.2", "1.2"),
-            ("0100:0.0", "100:0.0"),
+            ("00!1.2", "1.2"),
+            ("0100!0.0", "100!0.0"),
         ],
     )
     def test_normalized_versions(self, version, normalized):
@@ -204,9 +204,9 @@ def test_normalized_versions(self, version, normalized):
             ("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"),
+            ("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"),
+            ("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"),
@@ -216,26 +216,26 @@ def test_normalized_versions(self, version, normalized):
             ("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.0c1.dev456", "7:1.0c1.dev456"),
-            ("7:1.0c1", "7:1.0c1"),
-            ("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"),
+            ("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.0c1.dev456", "7!1.0c1.dev456"),
+            ("7!1.0c1", "7!1.0c1"),
+            ("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):
@@ -260,13 +260,13 @@ def test_version_hash(self, version):
             ("1.0a1.post5.dev6", "1.0a1.post5.dev6"),
             ("1.0rc4", "1.0c4"),
             ("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.0c4"),
-            ("1:1.0.post5", "1: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.0c4"),
+            ("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"),
@@ -274,13 +274,13 @@ def test_version_hash(self, version):
             ("1.0a1.post5.dev6+deadbeef", "1.0a1.post5.dev6"),
             ("1.0rc4+deadbeef", "1.0c4"),
             ("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.0c4"),
-            ("1:1.0.post5+deadbeef", "1: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.0c4"),
+            ("1!1.0.post5+deadbeef", "1!1.0.post5"),
         ],
     )
     def test_version_public(self, version, public):
@@ -296,13 +296,13 @@ def test_version_public(self, version, public):
             ("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!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"),
@@ -310,13 +310,13 @@ def test_version_public(self, version, public):
             ("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"),
+            ("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):
@@ -662,8 +662,8 @@ def test_specifiers_invalid(self, specifier):
             "1.0rc09",
             "1.0.post000",
             "1.1.dev09000",
-            "00:1.2",
-            "0100:0.0",
+            "00!1.2",
+            "0100!0.0",
         ],
     )
     def test_specifiers_normalized(self, version):
@@ -870,15 +870,15 @@ def test_comparison_non_specifier(self):
                 ("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"),
+                ("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"),
             ]
         ]
         +
@@ -973,13 +973,13 @@ def test_comparison_non_specifier(self):
                 ("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"),
+                ("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"),
             ]
         ],
     )

From c26a4272de2539c647f770f3c2cd25fa3cfe5539 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Wed, 25 Jun 2014 20:32:00 -0400
Subject: [PATCH 11/17] Allow empty specifiers

---
 packaging/version.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packaging/version.py b/packaging/version.py
index 2b37f62aa..5f67fa0c9 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -358,7 +358,7 @@ def __init__(self, specs, prereleases=False):
 
         # Split on comma to get each individual specification
         _specs = set()
-        for spec in specs.split(","):
+        for spec in (s for s in specs.split(",") if s):
             match = self._regex.search(spec)
             if not match:
                 raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec))

From 854565a26781fd10bb66b0d50a99ab239c193b59 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Thu, 3 Jul 2014 17:09:02 -0400
Subject: [PATCH 12/17] Implement LegacyVersion which can support arbitrary
 versions

---
 docs/version.rst      | 29 ++++++++++++++
 packaging/version.py  | 39 ++++++++++++++++++
 tests/test_version.py | 93 ++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 160 insertions(+), 1 deletion(-)

diff --git a/docs/version.rst b/docs/version.rst
index 74e40bd30..a2cbac8a6 100644
--- a/docs/version.rst
+++ b/docs/version.rst
@@ -82,6 +82,35 @@ Reference
         represents a prerelease or a final 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` however it is considered unorderable
+    and many of the comparison types are not implemented.
+
+    :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:: 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`.
+
+
 .. class:: Specifier(specifier)
 
     This class abstracts handling of specifying the dependencies of a project.
diff --git a/packaging/version.py b/packaging/version.py
index 5f67fa0c9..c879b88a9 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -36,6 +36,45 @@ class InvalidVersion(ValueError):
     """
 
 
+class LegacyVersion(object):
+
+    def __init__(self, version):
+        self._version = str(version)
+
+    def __str__(self):
+        return self._version
+
+    def __repr__(self):
+        return "".format(repr(str(self)))
+
+    def __hash__(self):
+        return hash(self._version)
+
+    def __eq__(self, other):
+        if not isinstance(other, LegacyVersion):
+            return NotImplemented
+
+        return self._version.lower() == other._version.lower()
+
+    def __ne__(self, other):
+        if not isinstance(other, LegacyVersion):
+            return NotImplemented
+
+        return self._version.lower() != other._version.lower()
+
+    @property
+    def public(self):
+        return self._version
+
+    @property
+    def local(self):
+        return None
+
+    @property
+    def is_prerelease(self):
+        return False
+
+
 class Version(object):
 
     _regex = re.compile(
diff --git a/tests/test_version.py b/tests/test_version.py
index 34e7f3d7b..d5c727081 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -21,7 +21,7 @@
 import pytest
 
 from packaging.version import (
-    Version, InvalidVersion, Specifier, InvalidSpecifier,
+    Version, LegacyVersion, InvalidVersion, Specifier, InvalidSpecifier,
 )
 
 
@@ -450,6 +450,97 @@ def test_compare_other(self, op, expected):
         assert getattr(operator, op)(Version("1"), other) is expected
 
 
+LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1-0"]
+
+
+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))
+                == "".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_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(
+        ("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
+
+
 # These should all be without spaces, we'll generate some with spaces using
 # these as templates.
 SPECIFIERS = [

From 38ddbb121cc420f84937f3165a9aa8e729671e80 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Thu, 3 Jul 2014 17:13:27 -0400
Subject: [PATCH 13/17] Rename identity to arbitrary and assure it operates on
 strings

---
 packaging/version.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index c879b88a9..b81f5be4f 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -388,7 +388,7 @@ class Specifier(object):
         ">=": "greater_than_equal",
         "<": "less_than",
         ">": "greater_than",
-        "===": "identity",
+        "===": "arbitrary",
     }
 
     def __init__(self, specs, prereleases=False):
@@ -556,8 +556,8 @@ def _compare_greater_than(self, prospective, spec):
         return (prospective > Version(spec)
                 and self._get_operator("!=")(prospective, spec + ".*"))
 
-    def _compare_identity(self, prospective, spec):
-        return prospective.lower() == spec.lower()
+    def _compare_arbitrary(self, prospective, spec):
+        return str(prospective).lower() == str(spec).lower()
 
 
 _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")

From a356cc9ad17d254e2ffbd3561e62222f18f8ae65 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Thu, 3 Jul 2014 17:17:45 -0400
Subject: [PATCH 14/17] Refactor normalization to handle LegacyVersion

---
 packaging/version.py | 31 ++++++++++++++++---------------
 1 file changed, 16 insertions(+), 15 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index b81f5be4f..132b938f7 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -443,24 +443,25 @@ def __ne__(self, other):
         return self._specs != other._specs
 
     def __contains__(self, item):
-        # Normalize item to a Version, this allows us to have a shortcut for
-        # ``"2.0" in Specifier(">=2")
-        version_item = item
-        if not isinstance(item, Version):
+        # Normalize item to a Version or LegacyVersion, this allows us to have
+        # a shortcut for ``"2.0" in Specifier(">=2")
+        if isinstance(item, (Version, LegacyVersion)):
+            version_item = item
+        else:
             try:
                 version_item = Version(item)
             except ValueError:
-                # If we cannot parse this as a version, then we can only
-                # support identity comparison so do a quick check to see if the
-                # spec contains any non identity specifiers
-                #
-                # This will return False if we do not have any specifiers, this
-                # is on purpose as a non PEP 440 version should require
-                # explicit opt in because otherwise they cannot be sanely
-                # prioritized
-                if (not self._specs
-                        or any(op != "===" for op, _ in self._specs)):
-                    return False
+                version_item = LegacyVersion(item)
+
+        # If we're operating on a LegacyVersion, then we can only support
+        # arbitrary comparison so do a quick check to see if the spec contains
+        # any non arbitrary specifiers
+        if isinstance(version_item, LegacyVersion):
+            # This will return False if we do not have any specifiers, this is
+            # on purpose as a non PEP 440 version should require explicit opt
+            # in because otherwise they cannot be sanely prioritized
+            if not self._specs or any(op != "===" for op, _ in self._specs):
+                return False
 
         # Ensure that the passed in version matches all of our version
         # specifiers

From 4fc2f3e3e0cbdff5722986611ac302cfbfa3f07a Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Mon, 4 Aug 2014 22:39:49 -0400
Subject: [PATCH 15/17] Relax the syntax of versions to better parse more
 versions

---
 packaging/version.py  | 93 +++++++++++++++++++++++++++++--------------
 tests/test_version.py | 13 ++++--
 2 files changed, 73 insertions(+), 33 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index 132b938f7..fc2fdc868 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -80,26 +80,32 @@ class Version(object):
     _regex = re.compile(
         r"""
         ^
+        \s*
+        v?
         (?:
-            (?:(?P[0-9]+)!)?               # epoch
-            (?P[0-9]+(?:\.[0-9]+)*)      # release segment
-            (?P
                              # pre release
-                [-\.]?
-                (?P(a|b|c|rc|alpha|beta))  #  - pre-release letter
-                (?P[0-9]+)?                #  - pre-release number
+            (?:(?P[0-9]+)!)?                           # epoch
+            (?P[0-9]+(?:\.[0-9]+)*)                  # release segment
+            (?P
                                          # pre-release
+                [-_\.]?
+                (?P(a|b|c|rc|alpha|beta|pre|preview))
+                [-_\.]?
+                (?P[0-9]+)?
             )?
-            (?P                             # post release
-                [-\.]?
-                (?Ppost)
+            (?P                                         # post release
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
                 (?P[0-9]+)?
             )?
-            (?P                              # dev release
-                [-\.]?
+            (?P                                          # dev release
+                [-_\.]?
                 (?Pdev)
+                [-_\.]?
                 (?P[0-9]+)?
             )?
         )
-        (?:\+(?P[a-z0-9]+(?:[a-z0-9\.]*[a-z0-9])?))? # local version
+        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+        \s*
         $
         """,
         re.VERBOSE | re.IGNORECASE,
@@ -232,12 +238,15 @@ def _parse_letter_version(letter, number):
             letter = "a"
         elif letter == "beta":
             letter = "b"
-        elif letter == "rc":
+        elif letter in ["rc", "pre", "preview"]:
             letter = "c"
 
         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").
@@ -245,7 +254,7 @@ def _parse_local_version(local):
     if local is not None:
         return tuple(
             part.lower() if not part.isdigit() else int(part)
-            for part in local.split(".")
+            for part in _local_version_seperators.split(local)
         )
 
 
@@ -313,6 +322,7 @@ class Specifier(object):
     _regex = re.compile(
         r"""
         ^
+        \s*
         (?P(~=|==|!=|<=|>=|<|>|===))
         (?P
             (?:
@@ -322,8 +332,9 @@ class Specifier(object):
                 # any semantic meaning from it. This operator is discouraged
                 # but included entirely as an escape hatch.
                 (?<====)  # Only match for the identity operator
-                .*        # We just match everything, since we are only testing
-                          # for strict identity.
+                \s*
+                [^\s]*    # We just match everything, except for whitespace
+                          # since we are only testing for strict identity.
             )
             |
             (?:
@@ -332,16 +343,23 @@ class Specifier(object):
                 # operators separately to enable that.
                 (?<===|!=)            # Only match for equals and not equals
 
+                \s*
+                v?
                 (?:[0-9]+!)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
-                (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
-                (?:[-\.]?post[0-9]*)? # post release
+                (?:                   # pre release
+                    [-_\.]?
+                    (a|b|c|rc|alpha|beta|pre|preview)
+                    [-_\.]?
+                    [0-9]*
+                )?
+                (?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)? # post release
 
                 # 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_\.+]*[a-z0-9])?)? # local
+                    (?:[-_\.]?dev[-_\.]?[0-9]*)?         # dev release
+                    (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
                     |
                     \.\*  # Wild card syntax of .*
                 )?
@@ -352,11 +370,18 @@ class Specifier(object):
                 # release segment.
                 (?<=~=)               # Only match for the compatible operator
 
+                \s*
+                v?
                 (?:[0-9]+!)?          # epoch
                 [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
-                (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
-                (?:[-\.]?post[0-9]*)? # post release
-                (?:[-\.]?dev[0-9]*)?  # dev release
+                (?:                   # pre release
+                    [-_\.]?
+                    (a|b|c|rc|alpha|beta|pre|preview)
+                    [-_\.]?
+                    [0-9]*
+                )?
+                (?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)? # post release
+                (?:[-_\.]?dev[-_\.]?[0-9]*)?          # dev release
             )
             |
             (?:
@@ -368,13 +393,21 @@ class Specifier(object):
                                       # operators so we want to make sure they
                                       # don't match here.
 
+                \s*
+                v?
                 (?:[0-9]+!)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
-                (?:[-\.]?(a|b|c|rc|alpha|beta)[0-9]*)? # pre release
-                (?:[-\.]?post[0-9]*)? # post release
-                (?:[-\.]?dev[0-9]*)?  # dev release
+                (?:                   # pre release
+                    [-_\.]?
+                    (a|b|c|rc|alpha|beta|pre|preview)
+                    [-_\.]?
+                    [0-9]*
+                )?
+                (?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)? # post release
+                (?:[-_\.]?dev[-_\.]?[0-9]*)?          # dev release
             )
         )
+        \s*
         $
         """,
         re.VERBOSE | re.IGNORECASE,
@@ -392,9 +425,6 @@ class Specifier(object):
     }
 
     def __init__(self, specs, prereleases=False):
-        # Normalize the specification to remove all of the whitespace
-        specs = specs.replace(" ", "")
-
         # Split on comma to get each individual specification
         _specs = set()
         for spec in (s for s in specs.split(",") if s):
@@ -403,7 +433,10 @@ def __init__(self, specs, prereleases=False):
                 raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec))
 
             _specs.add(
-                (match.group("operator"), match.group("version"))
+                (
+                    match.group("operator").strip(),
+                    match.group("version").strip(),
+                )
             )
 
         # Set a frozen set for our specifications
diff --git a/tests/test_version.py b/tests/test_version.py
index d5c727081..48ef5e0de 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -62,7 +62,6 @@ def test_valid_versions(self, version):
             "1.0+_foobar",
             "1.0+foo&asd",
             "1.0+1+1",
-            "1.0+1_1",
         ]
     )
     def test_invalid_versions(self, version):
@@ -181,6 +180,10 @@ def test_invalid_versions(self, version):
             ("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):
@@ -566,7 +569,7 @@ class TestSpecifier:
         # Do the same thing, except include spaces in the specifiers
         [
             ",".join([
-                " ".join(re.split(r"(~=|==|!=|<=|>=|<|>)", item)[1:])
+                " ".join(re.split(r"(===|~=|==|!=|<=|>=|<|>)", item)[1:])
                 for item in combination
             ])
             for combination in itertools.chain(*(
@@ -580,7 +583,7 @@ class TestSpecifier:
         [
             ",".join([
                 ("" if j % 2 else " ").join(
-                    re.split(r"(~=|==|!=|<=|>=|<|>)", item)[1:]
+                    re.split(r"(===|~=|==|!=|<=|>=|<|>)", item)[1:]
                 )
                 for j, item in enumerate(combination)
             ])
@@ -755,6 +758,10 @@ def test_specifiers_invalid(self, specifier):
             "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):

From 8de22d1824f83a29d0e464324e0aa19129216f71 Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Wed, 6 Aug 2014 10:18:29 -0400
Subject: [PATCH 16/17] Allow the implicit post release syntax (e.g. 1.0-5)

---
 packaging/version.py  | 32 ++++++++++++++++++++++++--------
 tests/test_version.py | 11 +++++++----
 2 files changed, 31 insertions(+), 12 deletions(-)

diff --git a/packaging/version.py b/packaging/version.py
index fc2fdc868..ec9f257e4 100644
--- a/packaging/version.py
+++ b/packaging/version.py
@@ -92,10 +92,14 @@ class Version(object):
                 (?P[0-9]+)?
             )?
             (?P                                         # post release
-                [-_\.]?
-                (?Ppost|rev|r)
-                [-_\.]?
-                (?P[0-9]+)?
+                (?:-(?P[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?Ppost|rev|r)
+                    [-_\.]?
+                    (?P[0-9]+)?
+                )
             )?
             (?P                                          # dev release
                 [-_\.]?
@@ -127,7 +131,7 @@ def __init__(self, version):
             ),
             post=_parse_letter_version(
                 match.group("post_l"),
-                match.group("post_n"),
+                match.group("post_n1") or match.group("post_n2"),
             ),
             dev=_parse_letter_version(
                 match.group("dev_l"),
@@ -242,6 +246,12 @@ def _parse_letter_version(letter, number):
             letter = "c"
 
         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"[\._-]")
@@ -353,7 +363,9 @@ class Specifier(object):
                     [-_\.]?
                     [0-9]*
                 )?
-                (?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)? # post release
+                (?:                   # 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.
@@ -380,7 +392,9 @@ class Specifier(object):
                     [-_\.]?
                     [0-9]*
                 )?
-                (?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)? # post release
+                (?:                                   # post release
+                    (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+                )?
                 (?:[-_\.]?dev[-_\.]?[0-9]*)?          # dev release
             )
             |
@@ -403,7 +417,9 @@ class Specifier(object):
                     [-_\.]?
                     [0-9]*
                 )?
-                (?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)? # post release
+                (?:                                   # post release
+                    (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+                )?
                 (?:[-_\.]?dev[-_\.]?[0-9]*)?          # dev release
             )
         )
diff --git a/tests/test_version.py b/tests/test_version.py
index 48ef5e0de..ca786b342 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -30,14 +30,15 @@
     # 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.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.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",
 
     # 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.0c1.dev456", "1!1.0c1", "1!1.0rc2", "1!1.0c3", "1!1.0",
+    "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",
@@ -166,6 +167,7 @@ def test_invalid_versions(self, version):
             ("1.0.POST1", "1.0.post1"),
             ("1.0-POST", "1.0.post0"),
             ("1.0-POST1", "1.0.post1"),
+            ("1.0-5", "1.0.post5"),
 
             # Local version case insensitivity
             ("1.0+AbC", "1.0+abc"),
@@ -744,6 +746,7 @@ def test_specifiers_invalid(self, specifier):
             "1.0.POST1",
             "1.0-POST",
             "1.0-POST1",
+            "1.0-5",
 
             # Local version case insensitivity
             "1.0+AbC"

From 44aec39328a5b876572566b5cfbe0e9e17f3967a Mon Sep 17 00:00:00 2001
From: Donald Stufft 
Date: Sat, 9 Aug 2014 01:03:45 -0400
Subject: [PATCH 17/17] Make the invoke tasks run on Python 2.x as well as
 Python 3.x

---
 tasks/check.py | 6 +++---
 tox.ini        | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/tasks/check.py b/tasks/check.py
index f3ebaf6fa..16532b8e9 100644
--- a/tasks/check.py
+++ b/tasks/check.py
@@ -57,10 +57,10 @@ def pep440(cached=False):
         bar = progress.bar.ShadyBar("Fetching Versions")
         client = xmlrpc_client.Server("https://pypi.python.org/pypi")
 
-        data = {
-            project: client.package_releases(project, True)
+        data = dict([
+            (project, client.package_releases(project, True))
             for project in bar.iter(client.list_packages())
-        }
+        ])
 
         os.makedirs(os.path.dirname(cache_path), exist_ok=True)
         with open(cache_path, "w") as fp:
diff --git a/tox.ini b/tox.ini
index c1b658831..06bfed5c2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -33,7 +33,7 @@ basepython = python2.6
 deps =
     flake8
     pep8-naming
-commands = flake8 . --exclude tasks/*,.tox,*.egg
+commands = flake8 .
 
 [flake8]
 exclude = .tox,*.egg