Merge pull request #1 from pypa/pep440

PEP 440 Implementation
pull/2/head
Donald Stufft 8 years ago
commit 0d92c43f35
  1. 3
      .coveragerc
  2. 14
      docs/index.rst
  3. 144
      docs/version.rst
  4. 27
      packaging/_compat.py
  5. 78
      packaging/_structures.py
  6. 651
      packaging/version.py
  7. 20
      tasks/__init__.py
  8. 153
      tasks/check.py
  9. 20
      tasks/paths.py
  10. 3
      tasks/requirements.txt
  11. 70
      tests/test_structures.py
  12. 1127
      tests/test_version.py
  13. 5
      tox.ini

@ -1,2 +1,3 @@
[run]
branch = True
branch = True
omit = packaging/_compat.py

@ -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

@ -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
<Version('1.0a5')>
>>> v2
<Version('1.0')>
>>> v1 < v2
True
>>> v1.is_prerelease
True
>>> v2.is_prerelease
False
>>> Version("french toast")
Traceback (most recent call last):
...
InvalidVersion: Invalid version: 'french toast'
>>> spec1 = Specifier("~=1.0")
>>> spec1
<Specifier('~=1.0')>
>>> spec2 = Specifier(">=1.0")
>>> spec2
<Specifier('>=1.0')>
>>> # We can combine specifiers
>>> combined_spec = spec1 & spec2
>>> combined_spec
<Specifier('>=1.0,~=1.0')>
>>> # We can also implicitly combine a string specifier
>>> combined_spec &= "!=1.1"
>>> combined_spec
<Specifier('!=1.1,>=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/

@ -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,

@ -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()

@ -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 "<LegacyVersion({0})>".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<epoch>[0-9]+)!)? # epoch
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_\.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
[-_\.]?
(?P<pre_n>[0-9]+)?
)?
(?P<post> # post release
(?:-(?P<post_n1>[0-9]+))
|
(?:
[-_\.]?
(?P<post_l>post|rev|r)
[-_\.]?
(?P<post_n2>[0-9]+)?
)
)?
(?P<dev> # dev release
[-_\.]?
(?P<dev_l>dev)
[-_\.]?
(?P<dev_n>[0-9]+)?
)?
)
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
\s*
$
""",
re.VERBOSE | re.IGNORECASE,
)
def __init__(self, version):
# Validate the version and parse it into pieces
match = self._regex.search(version)
if not match:
raise InvalidVersion("Invalid version: '{0}'".format(version))
# Store the parsed out pieces of the version
self._version = _Version(
epoch=int(match.group("epoch")) if match.group("epoch") else 0,
release=tuple(int(i) for i in match.group("release").split(".")),
pre=_parse_letter_version(
match.group("pre_l"),
match.group("pre_n"),
),
post=_parse_letter_version(
match.group("post_l"),
match.group("post_n1") or match.group("post_n2"),
),
dev=_parse_letter_version(
match.group("dev_l"),
match.group("dev_n"),
),
local=_parse_local_version(match.group("local")),
)
# Generate a key which will be used for sorting
self._key = _cmpkey(
self._version.epoch,
self._version.release,
self._version.pre,
self._version.post,
self._version.dev,
self._version.local,
)
def __repr__(self):
return "<Version({0})>".format(repr(str(self)))
def __str__(self):
parts = []
# Epoch
if self._version.epoch != 0:
parts.append("{0}!".format(self._version.epoch))
# Release segment
parts.append(".".join(str(x) for x in self._version.release))
# Pre-release
if self._version.pre is not None:
parts.append("".join(str(x) for x in self._version.pre))
# Post-release
if self._version.post is not None:
parts.append(".post{0}".format(self._version.post[1]))
# Development release
if self._version.dev is not None:
parts.append(".dev{0}".format(self._version.dev[1]))
# Local version segment
if self._version.local is not None:
parts.append(
"+{0}".format(".".join(str(x) for x in self._version.local))
)
return "".join(parts)
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<operator>(~=|==|!=|<=|>=|<|>|===))
(?P<version>
(?:
# The identity operators allow for an escape hatch that will
# do an exact string match of the version you wish to install.
# This will not be parsed by PEP 440 and we cannot determine
# any semantic meaning from it. This operator is discouraged
# but included entirely as an escape hatch.
(?<====) # Only match for the identity operator
\s*
[^\s]* # We just match everything, except for whitespace
# since we are only testing for strict identity.
)
|
(?:
# The (non)equality operators allow for wild card and local
# versions to be specified so we have to define these two
# operators separately to enable that.
(?<===|!=) # Only match for equals and not equals
\s*
v?
(?:[0-9]+!)? # epoch
[0-9]+(?:\.[0-9]+)* # release
(?: # pre release
[-_\.]?
(a|b|c|rc|alpha|beta|pre|preview)
[-_\.]?
[0-9]*
)?
(?: # post release
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
)?
# You cannot use a wild card and a dev or local version
# together so group them with a | and make them optional.
(?:
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
|
\.\* # Wild card syntax of .*
)?
)
|
(?:
# The compatible operator requires at least two digits in the
# release segment.
(?<=~=) # Only match for the compatible operator
\s*
v?
(?:[0-9]+!)? # epoch
[0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *)
(?: # pre release
[-_\.]?
(a|b|c|rc|alpha|beta|pre|preview)
[-_\.]?
[0-9]*
)?
(?: # post release
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
)?
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
)
|
(?:
# All other operators only allow a sub set of what the
# (non)equality operators do. Specifically they do not allow
# local versions to be specified nor do they allow the prefix
# matching wild cards.
(?<!==|!=|~=) # We have special cases for these
# operators so we want to make sure they
# don't match here.
\s*
v?
(?:[0-9]+!)? # epoch
[0-9]+(?:\.[0-9]+)* # release
(?: # pre release
[-_\.]?
(a|b|c|rc|alpha|beta|pre|preview)
[-_\.]?
[0-9]*
)?
(?: # post release
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
)?
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
)
)
\s*
$
""",
re.VERBOSE | re.IGNORECASE,
)
_operators = {
"~=": "compatible",
"==": "equal",
"!=": "not_equal",
"<=": "less_than_equal",
">=": "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 "<Specifier({0})>".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_greater_than(self, prospective, spec):
# Greater 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)),
)

@ -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)

@ -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),
)
)

@ -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")

@ -0,0 +1,3 @@
# The requirements required to invoke the tasks
invoke
progress

@ -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__)

@ -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"),