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/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..a2cbac8a6 --- /dev/null +++ b/docs/version.rst @@ -0,0 +1,144 @@ +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, Specifier + >>> 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' + >>> 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 +--------- + +.. 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:: 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. + 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/_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..ec9f257e4 --- /dev/null +++ b/packaging/version.py @@ -0,0 +1,651 @@ +# 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 ._compat import string_types +from ._structures import Infinity + + +__all__ = ["Version", "Specifier"] + + +_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 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( + 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|preview))
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+            (?P                                         # post release
+                (?:-(?P[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?Ppost|rev|r)
+                    [-_\.]?
+                    (?P[0-9]+)?
+                )
+            )?
+            (?P                                          # dev release
+                [-_\.]?
+                (?Pdev)
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+        )
+        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+        \s*
+        $
+        """,
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    def __init__(self, version):
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion("Invalid version: '{0}'".format(version))
+
+        # Store the parsed out pieces of the version
+        self._version = _Version(
+            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+            release=tuple(int(i) for i in match.group("release").split(".")),
+            pre=_parse_letter_version(
+                match.group("pre_l"),
+                match.group("pre_n"),
+            ),
+            post=_parse_letter_version(
+                match.group("post_l"),
+                match.group("post_n1") or match.group("post_n2"),
+            ),
+            dev=_parse_letter_version(
+                match.group("dev_l"),
+                match.group("dev_n"),
+            ),
+            local=_parse_local_version(match.group("local")),
+        )
+
+        # Generate a key which will be used for sorting
+        self._key = _cmpkey(
+            self._version.epoch,
+            self._version.release,
+            self._version.pre,
+            self._version.post,
+            self._version.dev,
+            self._version.local,
+        )
+
+    def __repr__(self):
+        return "".format(repr(str(self)))
+
+    def __str__(self):
+        parts = []
+
+        # Epoch
+        if self._version.epoch != 0:
+            parts.append("{0}!".format(self._version.epoch))
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self._version.release))
+
+        # Pre-release
+        if self._version.pre is not None:
+            parts.append("".join(str(x) for x in self._version.pre))
+
+        # Post-release
+        if self._version.post is not None:
+            parts.append(".post{0}".format(self._version.post[1]))
+
+        # Development release
+        if self._version.dev is not None:
+            parts.append(".dev{0}".format(self._version.dev[1]))
+
+        # Local version segment
+        if self._version.local is not None:
+            parts.append(
+                "+{0}".format(".".join(str(x) for x in self._version.local))
+            )
+
+        return "".join(parts)
+
+    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_letter_version(letter, number):
+    if letter:
+        # We consider there to be an implicit 0 in a pre-release if there is
+        # not a numeral associated with it.
+        if number is None:
+            number = 0
+
+        # We normalize any letters to their lower case form
+        letter = letter.lower()
+
+        # We consider some words to be alternate spellings of other words and
+        # in those cases we want to normalize the spellings to our preferred
+        # spelling.
+        if letter == "alpha":
+            letter = "a"
+        elif letter == "beta":
+            letter = "b"
+        elif letter in ["rc", "pre", "preview"]:
+            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"[\._-]")
+
+
+def _parse_local_version(local):
+    """
+    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+    """
+    if local is not None:
+        return tuple(
+            part.lower() if not part.isdigit() else int(part)
+            for part in _local_version_seperators.split(local)
+        )
+
+
+def _cmpkey(epoch, release, pre, post, dev, local):
+    # When we compare a release version, we want to compare it with all of the
+    # trailing zeros removed. So we'll use a reverse the list, drop all the now
+    # leading zeros until we come to something non zero, then take the rest
+    # re-reverse it back into the correct order and make it a tuple and use
+    # that for our sorting key.
+    release = tuple(
+        reversed(list(
+            itertools.dropwhile(
+                lambda x: x == 0,
+                reversed(release),
+            )
+        ))
+    )
+
+    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+    # We'll do this by abusing the pre segment, but we _only_ want to do this
+    # if there is not a pre or a post segment. If we have one of those then
+    # the normal sorting rules will handle this case correctly.
+    if pre is None and post is None and dev is not None:
+        pre = -Infinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        pre = Infinity
+
+    # Versions without a post segment should sort before those with one.
+    if post is None:
+        post = -Infinity
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        dev = Infinity
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        local = -Infinity
+    else:
+        # Versions with a local segment need that segment parsed to implement
+        # the sorting rules in PEP440.
+        # - Alpha numeric segments sort before numeric segments
+        # - Alpha numeric segments sort lexicographically
+        # - Numeric segments sort numerically
+        # - Shorter versions sort before longer versions when the prefixes
+        #   match exactly
+        local = tuple(
+            (i, "") if isinstance(i, int) else (-Infinity, i)
+            for i in local
+        )
+
+    return epoch, release, pre, post, dev, local
+
+
+class InvalidSpecifier(ValueError):
+    """
+    An invalid specifier was found, users should refer to PEP 440.
+    """
+
+
+class Specifier(object):
+
+    _regex = re.compile(
+        r"""
+        ^
+        \s*
+        (?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
+                \s*
+                [^\s]*    # We just match everything, except for whitespace
+                          # since we are only testing for strict identity.
+            )
+            |
+            (?:
+                # The (non)equality operators allow for wild card and local
+                # versions to be specified so we have to define these two
+                # operators separately to enable that.
+                (?<===|!=)            # Only match for equals and not equals
+
+                \s*
+                v?
+                (?:[0-9]+!)?          # epoch
+                [0-9]+(?:\.[0-9]+)*   # release
+                (?:                   # pre release
+                    [-_\.]?
+                    (a|b|c|rc|alpha|beta|pre|preview)
+                    [-_\.]?
+                    [0-9]*
+                )?
+                (?:                   # post release
+                    (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+                )?
+
+                # You cannot use a wild card and a dev or local version
+                # together so group them with a | and make them optional.
+                (?:
+                    (?:[-_\.]?dev[-_\.]?[0-9]*)?         # dev release
+                    (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
+                    |
+                    \.\*  # Wild card syntax of .*
+                )?
+            )
+            |
+            (?:
+                # The compatible operator requires at least two digits in the
+                # release segment.
+                (?<=~=)               # Only match for the compatible operator
+
+                \s*
+                v?
+                (?:[0-9]+!)?          # epoch
+                [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
+                (?:                   # pre release
+                    [-_\.]?
+                    (a|b|c|rc|alpha|beta|pre|preview)
+                    [-_\.]?
+                    [0-9]*
+                )?
+                (?:                                   # post release
+                    (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+                )?
+                (?:[-_\.]?dev[-_\.]?[0-9]*)?          # dev release
+            )
+            |
+            (?:
+                # All other operators only allow a sub set of what the
+                # (non)equality operators do. Specifically they do not allow
+                # local versions to be specified nor do they allow the prefix
+                # matching wild cards.
+                (?=": "greater_than_equal",
+        "<": "less_than",
+        ">": "greater_than",
+        "===": "arbitrary",
+    }
+
+    def __init__(self, specs, prereleases=False):
+        # Split on comma to get each individual specification
+        _specs = set()
+        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))
+
+            _specs.add(
+                (
+                    match.group("operator").strip(),
+                    match.group("version").strip(),
+                )
+            )
+
+        # 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 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:
+                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
+        return all(
+            self._get_operator(op)(
+                version_item if op != "===" else 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 + ".*"))
+
+    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]+)$")
+
+
+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/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..16532b8e9
--- /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 = 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:
+            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/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..ca786b342
--- /dev/null
+++ b/tests/test_version.py
@@ -0,0 +1,1127 @@
+# 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 re
+
+import pretend
+import pytest
+
+from packaging.version import (
+    Version, LegacyVersion, InvalidVersion, Specifier, InvalidSpecifier,
+)
+
+
+# This list must be in the correct sorting order
+VERSIONS = [
+    # Implicit epoch of 0
+    "1.0.dev456", "1.0a1", "1.0a2.dev456", "1.0a12.dev456", "1.0a12",
+    "1.0b1.dev456", "1.0b2", "1.0b2.post345.dev456", "1.0b2.post345",
+    "1.0b2-346", "1.0c1.dev456", "1.0c1", "1.0rc2", "1.0c3", "1.0",
+    "1.0.post456.dev34", "1.0.post456", "1.1.dev1", "1.2+123abc",
+    "1.2+123abc456", "1.2+abc", "1.2+abc123", "1.2+abc123def", "1.2+1234.abc",
+    "1.2+123456",
+
+    # Explicit epoch of 1
+    "1!1.0.dev456", "1!1.0a1", "1!1.0a2.dev456", "1!1.0a12.dev456", "1!1.0a12",
+    "1!1.0b1.dev456", "1!1.0b2", "1!1.0b2.post345.dev456", "1!1.0b2.post345",
+    "1!1.0b2-346", "1!1.0c1.dev456", "1!1.0c1", "1!1.0rc2", "1!1.0c3", "1!1.0",
+    "1!1.0.post456.dev34", "1!1.0.post456", "1!1.1.dev1", "1!1.2+123abc",
+    "1!1.2+123abc456", "1!1.2+abc", "1!1.2+abc123", "1!1.2+abc123def",
+    "1!1.2+1234.abc", "1!1.2+123456",
+]
+
+
+class TestVersion:
+
+    @pytest.mark.parametrize("version", VERSIONS)
+    def test_valid_versions(self, version):
+        Version(version)
+
+    @pytest.mark.parametrize(
+        "version",
+        [
+            # Non sensical versions should be invalid
+            "french toast",
+
+            # Versions with invalid local versions
+            "1.0+a+",
+            "1.0++",
+            "1.0+_foobar",
+            "1.0+foo&asd",
+            "1.0+1+1",
+        ]
+    )
+    def test_invalid_versions(self, version):
+        with pytest.raises(InvalidVersion):
+            Version(version)
+
+    @pytest.mark.parametrize(
+        ("version", "normalized"),
+        [
+            # Various development release incarnations
+            ("1.0dev", "1.0.dev0"),
+            ("1.0.dev", "1.0.dev0"),
+            ("1.0dev1", "1.0.dev1"),
+            ("1.0dev", "1.0.dev0"),
+            ("1.0-dev", "1.0.dev0"),
+            ("1.0-dev1", "1.0.dev1"),
+            ("1.0DEV", "1.0.dev0"),
+            ("1.0.DEV", "1.0.dev0"),
+            ("1.0DEV1", "1.0.dev1"),
+            ("1.0DEV", "1.0.dev0"),
+            ("1.0.DEV1", "1.0.dev1"),
+            ("1.0-DEV", "1.0.dev0"),
+            ("1.0-DEV1", "1.0.dev1"),
+
+            # Various alpha incarnations
+            ("1.0a", "1.0a0"),
+            ("1.0.a", "1.0a0"),
+            ("1.0.a1", "1.0a1"),
+            ("1.0-a", "1.0a0"),
+            ("1.0-a1", "1.0a1"),
+            ("1.0alpha", "1.0a0"),
+            ("1.0.alpha", "1.0a0"),
+            ("1.0.alpha1", "1.0a1"),
+            ("1.0-alpha", "1.0a0"),
+            ("1.0-alpha1", "1.0a1"),
+            ("1.0A", "1.0a0"),
+            ("1.0.A", "1.0a0"),
+            ("1.0.A1", "1.0a1"),
+            ("1.0-A", "1.0a0"),
+            ("1.0-A1", "1.0a1"),
+            ("1.0ALPHA", "1.0a0"),
+            ("1.0.ALPHA", "1.0a0"),
+            ("1.0.ALPHA1", "1.0a1"),
+            ("1.0-ALPHA", "1.0a0"),
+            ("1.0-ALPHA1", "1.0a1"),
+
+            # Various beta incarnations
+            ("1.0b", "1.0b0"),
+            ("1.0.b", "1.0b0"),
+            ("1.0.b1", "1.0b1"),
+            ("1.0-b", "1.0b0"),
+            ("1.0-b1", "1.0b1"),
+            ("1.0beta", "1.0b0"),
+            ("1.0.beta", "1.0b0"),
+            ("1.0.beta1", "1.0b1"),
+            ("1.0-beta", "1.0b0"),
+            ("1.0-beta1", "1.0b1"),
+            ("1.0B", "1.0b0"),
+            ("1.0.B", "1.0b0"),
+            ("1.0.B1", "1.0b1"),
+            ("1.0-B", "1.0b0"),
+            ("1.0-B1", "1.0b1"),
+            ("1.0BETA", "1.0b0"),
+            ("1.0.BETA", "1.0b0"),
+            ("1.0.BETA1", "1.0b1"),
+            ("1.0-BETA", "1.0b0"),
+            ("1.0-BETA1", "1.0b1"),
+
+            # Various release candidate incarnations
+            ("1.0c", "1.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"),
+
+            # 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"),
+            ("1.0-5", "1.0.post5"),
+
+            # Local version case insensitivity
+            ("1.0+AbC", "1.0+abc"),
+
+            # Integer Normalization
+            ("1.01", "1.1"),
+            ("1.0a05", "1.0a5"),
+            ("1.0b07", "1.0b7"),
+            ("1.0c056", "1.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.0"),
+
+            # Various other normalizations
+            ("v1.0", "1.0"),
+            ("   v1.0\t\n", "1.0"),
+        ],
+    )
+    def test_normalized_versions(self, version, normalized):
+        assert str(Version(version)) == normalized
+
+    @pytest.mark.parametrize(
+        ("version", "expected"),
+        [
+            ("1.0.dev456", "1.0.dev456"),
+            ("1.0a1", "1.0a1"),
+            ("1.0a2.dev456", "1.0a2.dev456"),
+            ("1.0a12.dev456", "1.0a12.dev456"),
+            ("1.0a12", "1.0a12"),
+            ("1.0b1.dev456", "1.0b1.dev456"),
+            ("1.0b2", "1.0b2"),
+            ("1.0b2.post345.dev456", "1.0b2.post345.dev456"),
+            ("1.0b2.post345", "1.0b2.post345"),
+            ("1.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"),
+            ("0!1.0.4+8.0", "1.0.4+8.0"),
+            ("1.0.5+9.5", "1.0.5+9.5"),
+            ("1.2+1234.abc", "1.2+1234.abc"),
+            ("1.2+123456", "1.2+123456"),
+            ("1.2+123abc", "1.2+123abc"),
+            ("1.2+123abc456", "1.2+123abc456"),
+            ("1.2+abc", "1.2+abc"),
+            ("1.2+abc123", "1.2+abc123"),
+            ("1.2+abc123def", "1.2+abc123def"),
+            ("1.1.dev1", "1.1.dev1"),
+            ("7!1.0.dev456", "7!1.0.dev456"),
+            ("7!1.0a1", "7!1.0a1"),
+            ("7!1.0a2.dev456", "7!1.0a2.dev456"),
+            ("7!1.0a12.dev456", "7!1.0a12.dev456"),
+            ("7!1.0a12", "7!1.0a12"),
+            ("7!1.0b1.dev456", "7!1.0b1.dev456"),
+            ("7!1.0b2", "7!1.0b2"),
+            ("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"),
+            ("7!1.0b2.post345", "7!1.0b2.post345"),
+            ("7!1.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):
+        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.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):
+        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
+
+
+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 = [
+    "~=2.0", "==2.1.*", "==2.1.0.3", "!=2.2.*", "!=2.2.0.5", "<=5", ">=7.9a1",
+    "<1.0.dev1", ">2.0.post1", "===lolwat",
+]
+
+
+class TestSpecifier:
+
+    @pytest.mark.parametrize(
+        "specifier",
+        # 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(
+        "version",
+        [
+            # Various development release incarnations
+            "1.0dev",
+            "1.0.dev",
+            "1.0dev1",
+            "1.0dev",
+            "1.0-dev",
+            "1.0-dev1",
+            "1.0DEV",
+            "1.0.DEV",
+            "1.0DEV1",
+            "1.0DEV",
+            "1.0.DEV1",
+            "1.0-DEV",
+            "1.0-DEV1",
+
+            # Various alpha incarnations
+            "1.0a",
+            "1.0.a",
+            "1.0.a1",
+            "1.0-a",
+            "1.0-a1",
+            "1.0alpha",
+            "1.0.alpha",
+            "1.0.alpha1",
+            "1.0-alpha",
+            "1.0-alpha1",
+            "1.0A",
+            "1.0.A",
+            "1.0.A1",
+            "1.0-A",
+            "1.0-A1",
+            "1.0ALPHA",
+            "1.0.ALPHA",
+            "1.0.ALPHA1",
+            "1.0-ALPHA",
+            "1.0-ALPHA1",
+
+            # Various beta incarnations
+            "1.0b",
+            "1.0.b",
+            "1.0.b1",
+            "1.0-b",
+            "1.0-b1",
+            "1.0beta",
+            "1.0.beta",
+            "1.0.beta1",
+            "1.0-beta",
+            "1.0-beta1",
+            "1.0B",
+            "1.0.B",
+            "1.0.B1",
+            "1.0-B",
+            "1.0-B1",
+            "1.0BETA",
+            "1.0.BETA",
+            "1.0.BETA1",
+            "1.0-BETA",
+            "1.0-BETA1",
+
+            # Various release candidate incarnations
+            "1.0c",
+            "1.0.c",
+            "1.0.c1",
+            "1.0-c",
+            "1.0-c1",
+            "1.0rc",
+            "1.0.rc",
+            "1.0.rc1",
+            "1.0-rc",
+            "1.0-rc1",
+            "1.0C",
+            "1.0.C",
+            "1.0.C1",
+            "1.0-C",
+            "1.0-C1",
+            "1.0RC",
+            "1.0.RC",
+            "1.0.RC1",
+            "1.0-RC",
+            "1.0-RC1",
+
+            # Various post release incarnations
+            "1.0post",
+            "1.0.post",
+            "1.0post1",
+            "1.0post",
+            "1.0-post",
+            "1.0-post1",
+            "1.0POST",
+            "1.0.POST",
+            "1.0POST1",
+            "1.0POST",
+            "1.0.POST1",
+            "1.0-POST",
+            "1.0-POST1",
+            "1.0-5",
+
+            # Local version case insensitivity
+            "1.0+AbC"
+
+            # Integer Normalization
+            "1.01",
+            "1.0a05",
+            "1.0b07",
+            "1.0c056",
+            "1.0rc09",
+            "1.0.post000",
+            "1.1.dev09000",
+            "00!1.2",
+            "0100!0.0",
+
+            # Various other normalizations
+            "v1.0",
+            "  \r \f \v v1.0\t\n",
+        ],
+    )
+    def test_specifiers_normalized(self, version):
+        if "+" not in version:
+            ops = ["~=", "==", "!=", "<=", ">=", "<", ">"]
+        else:
+            ops = ["==", "!="]
+
+        for op in ops:
+            Specifier(op + version)
+
+    @pytest.mark.parametrize(
+        ("specifier", "expected"),
+        [
+            # Single item specifiers should just be reflexive
+            ("!=2.0", "!=2.0"),
+            ("<2.0", "<2.0"),
+            ("<=2.0", "<=2.0"),
+            ("==2.0", "==2.0"),
+            (">2.0", ">2.0"),
+            (">=2.0", ">=2.0"),
+            ("~=2.0", "~=2.0"),
+
+            # 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
+
+    @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
diff --git a/tox.ini b/tox.ini
index 1735308ad..06bfed5c2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,10 +4,11 @@ envlist = py26,py27,pypy,py32,py33,py34,docs,pep8,py2pep8
 [testenv]
 deps =
     coverage
+    pretend
     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}