diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2014-12-13 10:36:55 -0500 |
---|---|---|
committer | Jason R. Coombs <jaraco@jaraco.com> | 2014-12-13 10:36:55 -0500 |
commit | 59f12c0141af1061de6f39f7b5a69795d1353993 (patch) | |
tree | 1743f53ecc5b06ea908806707fd75656ba3d9891 | |
parent | 3bc1dafb0e22f3c694c63cc1159648cee720e638 (diff) | |
parent | 2439ceb80430a4201efac7370c90474e429ab41b (diff) | |
download | python-setuptools-git-59f12c0141af1061de6f39f7b5a69795d1353993.tar.gz |
Merge with 8.0b1 (use packaging for version specifiers)8.0
-rw-r--r-- | .hgtags | 1 | ||||
-rw-r--r-- | CHANGES.txt | 13 | ||||
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | docs/pkg_resources.txt | 66 | ||||
-rw-r--r-- | ez_setup.py | 2 | ||||
-rw-r--r-- | pkg_resources.py | 163 | ||||
-rw-r--r-- | setuptools/_vendor/__init__.py | 0 | ||||
-rw-r--r-- | setuptools/_vendor/packaging/__about__.py | 31 | ||||
-rw-r--r-- | setuptools/_vendor/packaging/__init__.py | 24 | ||||
-rw-r--r-- | setuptools/_vendor/packaging/_compat.py | 40 | ||||
-rw-r--r-- | setuptools/_vendor/packaging/_structures.py | 78 | ||||
-rw-r--r-- | setuptools/_vendor/packaging/specifiers.py | 732 | ||||
-rw-r--r-- | setuptools/_vendor/packaging/version.py | 376 | ||||
-rw-r--r-- | setuptools/_vendor/vendored.txt | 1 | ||||
-rwxr-xr-x | setuptools/command/egg_info.py | 16 | ||||
-rw-r--r-- | setuptools/dist.py | 29 | ||||
-rw-r--r-- | setuptools/tests/test_egg_info.py | 2 | ||||
-rw-r--r-- | setuptools/tests/test_resources.py | 37 | ||||
-rw-r--r-- | setuptools/version.py | 2 |
19 files changed, 1425 insertions, 195 deletions
@@ -160,3 +160,4 @@ bc6655b4acf205dd9f25c702955645656077398a 6.0.1 01271e84e5125fcc4f0f368a6e21116a5722953c 6.0.2 7ea80190d494a766c6356fce85c844703964b6cc 6.1 df26609c2f614f5fc9110342e4003ee8bd95cf84 7.0 +850a5c155c48b6ecfbb83b961586ea359b561522 8.0b1 diff --git a/CHANGES.txt b/CHANGES.txt index 49529a04..cc2423b6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,19 @@ CHANGES ======= --- +8.0 +--- + +* Implement `PEP 440 <http://legacy.python.org/dev/peps/pep-0440/>`_ within + pkg_resources and setuptools. This change + deprecates some version numbers such that they will no longer be installable + without using the ``===`` escape hatch. See `the changes to test_resources + <https://bitbucket.org/pypa/setuptools/commits/dcd552da643c4448056de84c73d56da6d70769d5#chg-setuptools/tests/test_resources.py>`_ + for specific examples of version numbers and specifiers that are no longer + supported. Setuptools now "vendors" the `packaging + <https://github.com/pypa/packaging>`_ library. + +--- 7.0 --- diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ab60b882 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +empty: + exit 1 + +update-vendored: + rm -rf setuptools/_vendor/packaging + pip install -r setuptools/_vendor/vendored.txt -t setuptools/_vendor/ + rm -rf setuptools/_vendor/*.{egg,dist}-info diff --git a/docs/pkg_resources.txt b/docs/pkg_resources.txt index f4a768e4..6c6405a8 100644 --- a/docs/pkg_resources.txt +++ b/docs/pkg_resources.txt @@ -594,7 +594,7 @@ Requirements Parsing requirement ::= project_name versionspec? extras? versionspec ::= comparison version (',' comparison version)* - comparison ::= '<' | '<=' | '!=' | '==' | '>=' | '>' + comparison ::= '<' | '<=' | '!=' | '==' | '>=' | '>' | '~=' | '===' extras ::= '[' extralist? ']' extralist ::= identifier (',' identifier)* project_name ::= identifier @@ -646,13 +646,10 @@ Requirements Parsing The ``Requirement`` object's version specifiers (``.specs``) are internally sorted into ascending version order, and used to establish what ranges of versions are acceptable. Adjacent redundant conditions are effectively - consolidated (e.g. ``">1, >2"`` produces the same results as ``">1"``, and - ``"<2,<3"`` produces the same results as``"<3"``). ``"!="`` versions are + consolidated (e.g. ``">1, >2"`` produces the same results as ``">2"``, and + ``"<2,<3"`` produces the same results as``"<2"``). ``"!="`` versions are excised from the ranges they fall within. The version being tested for acceptability is then checked for membership in the resulting ranges. - (Note that providing conflicting conditions for the same version (e.g. - ``"<2,>=2"`` or ``"==2,!=2"``) is meaningless and may therefore produce - bizarre results when compared with actual version number(s).) ``__eq__(other_requirement)`` A requirement compares equal to another requirement if they have @@ -681,10 +678,7 @@ Requirements Parsing ``specs`` A list of ``(op,version)`` tuples, sorted in ascending parsed-version order. The `op` in each tuple is a comparison operator, represented as - a string. The `version` is the (unparsed) version number. The relative - order of tuples containing the same version numbers is undefined, since - having more than one operator for a given version is either redundant or - self-contradictory. + a string. The `version` is the (unparsed) version number. Entry Points @@ -967,7 +961,7 @@ version ``ValueError`` is raised. parsed_version - The ``parsed_version`` is a tuple representing a "parsed" form of the + The ``parsed_version`` is an object representing a "parsed" form of the distribution's ``version``. ``dist.parsed_version`` is a shortcut for calling ``parse_version(dist.version)``. It is used to compare or sort distributions by version. (See the `Parsing Utilities`_ section below for @@ -1541,40 +1535,12 @@ Parsing Utilities ----------------- ``parse_version(version)`` - Parse a project's version string, returning a value that can be used to - compare versions by chronological order. Semantically, the format is a - rough cross between distutils' ``StrictVersion`` and ``LooseVersion`` - classes; if you give it versions that would work with ``StrictVersion``, - then they will compare the same way. Otherwise, comparisons are more like - a "smarter" form of ``LooseVersion``. It is *possible* to create - pathological version coding schemes that will fool this parser, but they - should be very rare in practice. - - The returned value will be a tuple of strings. Numeric portions of the - version are padded to 8 digits so they will compare numerically, but - without relying on how numbers compare relative to strings. Dots are - dropped, but dashes are retained. Trailing zeros between alpha segments - or dashes are suppressed, so that e.g. "2.4.0" is considered the same as - "2.4". Alphanumeric parts are lower-cased. - - The algorithm assumes that strings like "-" and any alpha string that - alphabetically follows "final" represents a "patch level". So, "2.4-1" - is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is - considered newer than "2.4-1", which in turn is newer than "2.4". - - Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that - come before "final" alphabetically) are assumed to be pre-release versions, - so that the version "2.4" is considered newer than "2.4a1". Any "-" - characters preceding a pre-release indicator are removed. (In versions of - setuptools prior to 0.6a9, "-" characters were not removed, leading to the - unintuitive result that "0.2-rc1" was considered a newer version than - "0.2".) - - Finally, to handle miscellaneous cases, the strings "pre", "preview", and - "rc" are treated as if they were "c", i.e. as though they were release - candidates, and therefore are not as new as a version string that does not - contain them. And the string "dev" is treated as if it were an "@" sign; - that is, a version coming before even "a" or "alpha". + Parsed a project's version string as defined by PEP 440. The returned + value will be an object that represents the version. These objects may + be compared to each other and sorted. The sorting algorithm is as defined + by PEP 440 with the addition that any version which is not a valid PEP 440 + version will be considered less than any valid PEP 440 version and the + invalid versions will continue sorting using the original algorithm. .. _yield_lines(): @@ -1629,10 +1595,12 @@ Parsing Utilities See ``to_filename()``. ``safe_version(version)`` - Similar to ``safe_name()`` except that spaces in the input become dots, and - dots are allowed to exist in the output. As with ``safe_name()``, if you - are generating a filename from this you should replace any "-" characters - in the output with underscores. + This will return the normalized form of any PEP 440 version, if the version + string is not PEP 440 compatible than it is similar to ``safe_name()`` + except that spaces in the input become dots, and dots are allowed to exist + in the output. As with ``safe_name()``, if you are generating a filename + from this you should replace any "-" characters in the output with + underscores. ``safe_extra(extra)`` Return a "safe" form of an extra's name, suitable for use in a requirement diff --git a/ez_setup.py b/ez_setup.py index 86647613..f6361e8e 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -36,7 +36,7 @@ try: except ImportError: USER_SITE = None -DEFAULT_VERSION = "7.1" +DEFAULT_VERSION = "8.0" DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" def _python_cmd(*args): diff --git a/pkg_resources.py b/pkg_resources.py index 511068a6..a3eef28f 100644 --- a/pkg_resources.py +++ b/pkg_resources.py @@ -14,6 +14,8 @@ The package resource API is designed to work with normal filesystem packages, method. """ +from __future__ import absolute_import + import sys import os import io @@ -73,6 +75,13 @@ try: except ImportError: pass +import setuptools._vendor.packaging.version +import setuptools._vendor.packaging.specifiers +packaging = setuptools._vendor.packaging + +# For compatibility, expose packaging.version.parse as parse_version +parse_version = packaging.version.parse + _state_vars = {} @@ -1156,13 +1165,15 @@ def safe_name(name): def safe_version(version): - """Convert an arbitrary string to a standard version string - - Spaces become dots, and all other non-alphanumeric characters become - dashes, with runs of multiple dashes condensed to a single dash. """ - version = version.replace(' ','.') - return re.sub('[^A-Za-z0-9.]+', '-', version) + Convert an arbitrary string to a standard version string + """ + try: + # normalize the version + return str(packaging.version.Version(version)) + except packaging.version.InvalidVersion: + version = version.replace(' ','.') + return re.sub('[^A-Za-z0-9.]+', '-', version) def safe_extra(extra): @@ -2080,7 +2091,7 @@ CONTINUE = re.compile(r"\s*\\\s*(#.*)?$").match # Distribution or extra DISTRO = re.compile(r"\s*((\w|[-.])+)").match # ver. info -VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|[-.])+)").match +VERSION = re.compile(r"\s*(<=?|>=?|===?|!=|~=)\s*((\w|[-.*_!+])+)").match # comma between items COMMA = re.compile(r"\s*,").match OBRACKET = re.compile(r"\s*\[").match @@ -2092,67 +2103,6 @@ EGG_NAME = re.compile( re.VERBOSE | re.IGNORECASE ).match -component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE) -replace = {'pre':'c', 'preview':'c','-':'final-','rc':'c','dev':'@'}.get - -def _parse_version_parts(s): - for part in component_re.split(s): - part = replace(part, part) - if not part or part=='.': - continue - if part[:1] in '0123456789': - # pad for numeric comparison - yield part.zfill(8) - else: - yield '*'+part - - # ensure that alpha/beta/candidate are before final - yield '*final' - -def parse_version(s): - """Convert a version string to a chronologically-sortable key - - This is a rough cross between distutils' StrictVersion and LooseVersion; - if you give it versions that would work with StrictVersion, then it behaves - the same; otherwise it acts like a slightly-smarter LooseVersion. It is - *possible* to create pathological version coding schemes that will fool - this parser, but they should be very rare in practice. - - The returned value will be a tuple of strings. Numeric portions of the - version are padded to 8 digits so they will compare numerically, but - without relying on how numbers compare relative to strings. Dots are - dropped, but dashes are retained. Trailing zeros between alpha segments - or dashes are suppressed, so that e.g. "2.4.0" is considered the same as - "2.4". Alphanumeric parts are lower-cased. - - The algorithm assumes that strings like "-" and any alpha string that - alphabetically follows "final" represents a "patch level". So, "2.4-1" - is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is - considered newer than "2.4-1", which in turn is newer than "2.4". - - Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that - come before "final" alphabetically) are assumed to be pre-release versions, - so that the version "2.4" is considered newer than "2.4a1". - - Finally, to handle miscellaneous cases, the strings "pre", "preview", and - "rc" are treated as if they were "c", i.e. as though they were release - candidates, and therefore are not as new as a version string that does not - contain them, and "dev" is replaced with an '@' so that it sorts lower than - than any other pre-release tag. - """ - parts = [] - for part in _parse_version_parts(s.lower()): - if part.startswith('*'): - # remove '-' before a prerelease tag - if part < '*final': - while parts and parts[-1] == '*final-': - parts.pop() - # remove trailing zeros from each series of numeric parts - while parts and parts[-1]=='00000000': - parts.pop() - parts.append(part) - return tuple(parts) - class EntryPoint(object): """Object representing an advertised importable object""" @@ -2305,7 +2255,7 @@ class Distribution(object): @property def hashcmp(self): return ( - getattr(self, 'parsed_version', ()), + self.parsed_version, self.precedence, self.key, _remove_md5_fragment(self.location), @@ -2351,11 +2301,10 @@ class Distribution(object): @property def parsed_version(self): - try: - return self._parsed_version - except AttributeError: - self._parsed_version = pv = parse_version(self.version) - return pv + if not hasattr(self, "_parsed_version"): + self._parsed_version = parse_version(self.version) + + return self._parsed_version @property def version(self): @@ -2460,7 +2409,12 @@ class Distribution(object): def as_requirement(self): """Return a ``Requirement`` that matches this distribution exactly""" - return Requirement.parse('%s==%s' % (self.project_name, self.version)) + if isinstance(self.parsed_version, packaging.version.Version): + spec = "%s==%s" % (self.project_name, self.parsed_version) + else: + spec = "%s===%s" % (self.project_name, self.parsed_version) + + return Requirement.parse(spec) def load_entry_point(self, group, name): """Return the `name` entry point of `group` or raise ImportError""" @@ -2712,7 +2666,7 @@ def parse_requirements(strs): line, p, specs = scan_list(VERSION, LINE_END, line, p, (1, 2), "version spec") - specs = [(op, safe_version(val)) for op, val in specs] + specs = [(op, val) for op, val in specs] yield Requirement(project_name, specs, extras) @@ -2721,26 +2675,23 @@ class Requirement: """DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!""" self.unsafe_name, project_name = project_name, safe_name(project_name) self.project_name, self.key = project_name, project_name.lower() - index = [ - (parse_version(v), state_machine[op], op, v) - for op, v in specs - ] - index.sort() - self.specs = [(op, ver) for parsed, trans, op, ver in index] - self.index, self.extras = index, tuple(map(safe_extra, extras)) + self.specifier = packaging.specifiers.SpecifierSet( + ",".join(["".join([x, y]) for x, y in specs]) + ) + self.specs = specs + self.extras = tuple(map(safe_extra, extras)) self.hashCmp = ( self.key, - tuple((op, parsed) for parsed, trans, op, ver in index), + self.specifier, frozenset(self.extras), ) self.__hash = hash(self.hashCmp) def __str__(self): - specs = ','.join([''.join(s) for s in self.specs]) extras = ','.join(self.extras) if extras: extras = '[%s]' % extras - return '%s%s%s' % (self.project_name, extras, specs) + return '%s%s%s' % (self.project_name, extras, self.specifier) def __eq__(self, other): return ( @@ -2752,29 +2703,13 @@ class Requirement: if isinstance(item, Distribution): if item.key != self.key: return False - # only get if we need it - if self.index: - item = item.parsed_version - elif isinstance(item, string_types): - item = parse_version(item) - last = None - # -1, 0, 1 - compare = lambda a, b: (a > b) - (a < b) - for parsed, trans, op, ver in self.index: - # Indexing: 0, 1, -1 - action = trans[compare(item, parsed)] - if action == 'F': - return False - elif action == 'T': - return True - elif action == '+': - last = True - elif action == '-' or last is None: - last = False - # no rules encountered - if last is None: - last = True - return last + + item = item.version + + # Allow prereleases always in order to match the previous behavior of + # this method. In the future this should be smarter and follow PEP 440 + # more accurately. + return self.specifier.contains(item, prereleases=True) def __hash__(self): return self.__hash @@ -2790,16 +2725,6 @@ class Requirement: raise ValueError("Expected only one requirement", s) raise ValueError("No requirements found", s) -state_machine = { - # =>< - '<': '--T', - '<=': 'T-T', - '>': 'F+F', - '>=': 'T+F', - '==': 'T..', - '!=': 'F++', -} - def _get_mro(cls): """Get an mro for a type or classic class""" diff --git a/setuptools/_vendor/__init__.py b/setuptools/_vendor/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/setuptools/_vendor/__init__.py diff --git a/setuptools/_vendor/packaging/__about__.py b/setuptools/_vendor/packaging/__about__.py new file mode 100644 index 00000000..481589e7 --- /dev/null +++ b/setuptools/_vendor/packaging/__about__.py @@ -0,0 +1,31 @@ +# 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 + +__all__ = [ + "__title__", "__summary__", "__uri__", "__version__", "__author__", + "__email__", "__license__", "__copyright__", +] + +__title__ = "packaging" +__summary__ = "Core utilities for Python packages" +__uri__ = "https://github.com/pypa/packaging" + +__version__ = "14.3" + +__author__ = "Donald Stufft" +__email__ = "donald@stufft.io" + +__license__ = "Apache License, Version 2.0" +__copyright__ = "Copyright 2014 %s" % __author__ diff --git a/setuptools/_vendor/packaging/__init__.py b/setuptools/_vendor/packaging/__init__.py new file mode 100644 index 00000000..c39a8eab --- /dev/null +++ b/setuptools/_vendor/packaging/__init__.py @@ -0,0 +1,24 @@ +# 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 + +from .__about__ import ( + __author__, __copyright__, __email__, __license__, __summary__, __title__, + __uri__, __version__ +) + +__all__ = [ + "__title__", "__summary__", "__uri__", "__version__", "__author__", + "__email__", "__license__", "__copyright__", +] diff --git a/setuptools/_vendor/packaging/_compat.py b/setuptools/_vendor/packaging/_compat.py new file mode 100644 index 00000000..5c396cea --- /dev/null +++ b/setuptools/_vendor/packaging/_compat.py @@ -0,0 +1,40 @@ +# 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, + + +def with_metaclass(meta, *bases): + """ + Create a base class with a metaclass. + """ + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) diff --git a/setuptools/_vendor/packaging/_structures.py b/setuptools/_vendor/packaging/_structures.py new file mode 100644 index 00000000..0ae9bb52 --- /dev/null +++ b/setuptools/_vendor/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/setuptools/_vendor/packaging/specifiers.py b/setuptools/_vendor/packaging/specifiers.py new file mode 100644 index 00000000..bea4a398 --- /dev/null +++ b/setuptools/_vendor/packaging/specifiers.py @@ -0,0 +1,732 @@ +# 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 abc +import functools +import itertools +import re + +from ._compat import string_types, with_metaclass +from .version import Version, LegacyVersion, parse + + +class InvalidSpecifier(ValueError): + """ + An invalid specifier was found, users should refer to PEP 440. + """ + + +class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): + + @abc.abstractmethod + def __str__(self): + """ + Returns the str representation of this Specifier like object. This + should be representative of the Specifier itself. + """ + + @abc.abstractmethod + def __hash__(self): + """ + Returns a hash value for this Specifier like object. + """ + + @abc.abstractmethod + def __eq__(self, other): + """ + Returns a boolean representing whether or not the two Specifier like + objects are equal. + """ + + @abc.abstractmethod + def __ne__(self, other): + """ + Returns a boolean representing whether or not the two Specifier like + objects are not equal. + """ + + @abc.abstractproperty + def prereleases(self): + """ + Returns whether or not pre-releases as a whole are allowed by this + specifier. + """ + + @prereleases.setter + def prereleases(self, value): + """ + Sets whether or not pre-releases as a whole are allowed by this + specifier. + """ + + @abc.abstractmethod + def contains(self, item, prereleases=None): + """ + Determines if the given item is contained within this specifier. + """ + + @abc.abstractmethod + def filter(self, iterable, prereleases=None): + """ + Takes an iterable of items and filters them so that only items which + are contained within this specifier are allowed in it. + """ + + +class _IndividualSpecifier(BaseSpecifier): + + _operators = {} + + def __init__(self, spec="", prereleases=None): + match = self._regex.search(spec) + if not match: + raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec)) + + self._spec = ( + match.group("operator").strip(), + match.group("version").strip(), + ) + + # Store whether or not this Specifier should accept prereleases + self._prereleases = prereleases + + def __repr__(self): + pre = ( + ", prereleases={0!r}".format(self.prereleases) + if self._prereleases is not None + else "" + ) + + return "<{0}({1!r}{2})>".format( + self.__class__.__name__, + str(self), + pre, + ) + + def __str__(self): + return "{0}{1}".format(*self._spec) + + def __hash__(self): + return hash(self._spec) + + def __eq__(self, other): + if isinstance(other, string_types): + try: + other = self.__class__(other) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._spec == other._spec + + def __ne__(self, other): + if isinstance(other, string_types): + try: + other = self.__class__(other) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._spec != other._spec + + def _get_operator(self, op): + return getattr(self, "_compare_{0}".format(self._operators[op])) + + def _coerce_version(self, version): + if not isinstance(version, (LegacyVersion, Version)): + version = parse(version) + return version + + @property + def prereleases(self): + return self._prereleases + + @prereleases.setter + def prereleases(self, value): + self._prereleases = value + + def contains(self, item, prereleases=None): + # Determine if prereleases are to be allowed or not. + if prereleases is None: + prereleases = self.prereleases + + # Normalize item to a Version or LegacyVersion, this allows us to have + # a shortcut for ``"2.0" in Specifier(">=2") + item = self._coerce_version(item) + + # Determine if we should be supporting prereleases in this specifier + # or not, if we do not support prereleases than we can short circuit + # logic if this version is a prereleases. + if item.is_prerelease and not prereleases: + return False + + # Actually do the comparison to determine if this item is contained + # within this Specifier or not. + return self._get_operator(self._spec[0])(item, self._spec[1]) + + def filter(self, iterable, prereleases=None): + yielded = False + found_prereleases = [] + + kw = {"prereleases": prereleases if prereleases is not None else True} + + # Attempt to iterate over all the values in the iterable and if any of + # them match, yield them. + for version in iterable: + parsed_version = self._coerce_version(version) + + if self.contains(parsed_version, **kw): + # If our version is a prerelease, and we were not set to allow + # prereleases, then we'll store it for later incase nothing + # else matches this specifier. + if (parsed_version.is_prerelease + and not (prereleases or self.prereleases)): + found_prereleases.append(version) + # Either this is not a prerelease, or we should have been + # accepting prereleases from the begining. + else: + yielded = True + yield version + + # Now that we've iterated over everything, determine if we've yielded + # any values, and if we have not and we have any prereleases stored up + # then we will go ahead and yield the prereleases. + if not yielded and found_prereleases: + for version in found_prereleases: + yield version + + +class LegacySpecifier(_IndividualSpecifier): + + _regex = re.compile( + r""" + ^ + \s* + (?P<operator>(==|!=|<=|>=|<|>)) + \s* + (?P<version> + [^\s]* # We just match everything, except for whitespace since this + # is a "legacy" specifier and the version string can be just + # about anything. + ) + \s* + $ + """, + re.VERBOSE | re.IGNORECASE, + ) + + _operators = { + "==": "equal", + "!=": "not_equal", + "<=": "less_than_equal", + ">=": "greater_than_equal", + "<": "less_than", + ">": "greater_than", + } + + def _coerce_version(self, version): + if not isinstance(version, LegacyVersion): + version = LegacyVersion(str(version)) + return version + + def _compare_equal(self, prospective, spec): + return prospective == self._coerce_version(spec) + + def _compare_not_equal(self, prospective, spec): + return prospective != self._coerce_version(spec) + + def _compare_less_than_equal(self, prospective, spec): + return prospective <= self._coerce_version(spec) + + def _compare_greater_than_equal(self, prospective, spec): + return prospective >= self._coerce_version(spec) + + def _compare_less_than(self, prospective, spec): + return prospective < self._coerce_version(spec) + + def _compare_greater_than(self, prospective, spec): + return prospective > self._coerce_version(spec) + + +def _require_version_compare(fn): + @functools.wraps(fn) + def wrapped(self, prospective, spec): + if not isinstance(prospective, Version): + return False + return fn(self, prospective, spec) + return wrapped + + +class Specifier(_IndividualSpecifier): + + _regex = 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", + } + + @_require_version_compare + def _compare_compatible(self, prospective, spec): + # Compatible releases have an equivalent combination of >= and ==. That + # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to + # implement this in terms of the other specifiers instead of + # implementing it ourselves. The only thing we need to do is construct + # the other specifiers. + + # We want everything but the last item in the version, but we want to + # ignore post and dev releases and we want to treat the pre-release as + # it's own separate segment. + prefix = ".".join( + list( + itertools.takewhile( + lambda x: (not x.startswith("post") + and not x.startswith("dev")), + _version_split(spec), + ) + )[:-1] + ) + + # Add the prefix notation to the end of our string + prefix += ".*" + + return (self._get_operator(">=")(prospective, spec) + and self._get_operator("==")(prospective, prefix)) + + @_require_version_compare + def _compare_equal(self, prospective, spec): + # We need special logic to handle prefix matching + if spec.endswith(".*"): + # Split the spec out by dots, and pretend that there is an implicit + # dot in between a release segment and a pre-release segment. + spec = _version_split(spec[:-2]) # Remove the trailing .* + + # Split the prospective version out by dots, and pretend that there + # is an implicit dot in between a release segment and a pre-release + # segment. + prospective = _version_split(str(prospective)) + + # Shorten the prospective version to be the same length as the spec + # so that we can determine if the specifier is a prefix of the + # prospective version or not. + prospective = prospective[:len(spec)] + + # Pad out our two sides with zeros so that they both equal the same + # length. + spec, prospective = _pad_version(spec, prospective) + else: + # Convert our spec string into a Version + spec = Version(spec) + + # If the specifier does not have a local segment, then we want to + # act as if the prospective version also does not have a local + # segment. + if not spec.local: + prospective = Version(prospective.public) + + return prospective == spec + + @_require_version_compare + def _compare_not_equal(self, prospective, spec): + return not self._compare_equal(prospective, spec) + + @_require_version_compare + def _compare_less_than_equal(self, prospective, spec): + return prospective <= Version(spec) + + @_require_version_compare + def _compare_greater_than_equal(self, prospective, spec): + return prospective >= Version(spec) + + @_require_version_compare + def _compare_less_than(self, prospective, spec): + # 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 + ".*")) + + @_require_version_compare + 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() + + @property + def prereleases(self): + # If there is an explicit prereleases set for this, then we'll just + # blindly use that. + if self._prereleases is not None: + return self._prereleases + + # Look at all of our specifiers and determine if they are inclusive + # operators, and if they are if they are including an explicit + # prerelease. + operator, version = self._spec + if operator in ["==", ">=", "<=", "~="]: + # The == specifier can include a trailing .*, if it does we + # want to remove before parsing. + if operator == "==" and version.endswith(".*"): + version = version[:-2] + + # Parse the version, and if it is a pre-release than this + # specifier allows pre-releases. + if parse(version).is_prerelease: + return True + + return False + + @prereleases.setter + def prereleases(self, value): + self._prereleases = value + + +_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") + + +def _version_split(version): + result = [] + for item in version.split("."): + match = _prefix_regex.search(item) + if match: + result.extend(match.groups()) + else: + result.append(item) + return result + + +def _pad_version(left, right): + left_split, right_split = [], [] + + # Get the release segment of our versions + left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) + right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) + + # Get the rest of our versions + left_split.append(left[len(left_split):]) + 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)), + ) + + +class SpecifierSet(BaseSpecifier): + + def __init__(self, specifiers="", prereleases=None): + # Split on , to break each indidivual specifier into it's own item, and + # strip each item to remove leading/trailing whitespace. + specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + + # Parsed each individual specifier, attempting first to make it a + # Specifier and falling back to a LegacySpecifier. + parsed = set() + for specifier in specifiers: + try: + parsed.add(Specifier(specifier)) + except InvalidSpecifier: + parsed.add(LegacySpecifier(specifier)) + + # Turn our parsed specifiers into a frozen set and save them for later. + self._specs = frozenset(parsed) + + # Store our prereleases value so we can use it later to determine if + # we accept prereleases or not. + self._prereleases = prereleases + + def __repr__(self): + pre = ( + ", prereleases={0!r}".format(self.prereleases) + if self._prereleases is not None + else "" + ) + + return "<SpecifierSet({0!r}{1})>".format(str(self), pre) + + def __str__(self): + return ",".join(sorted(str(s) for s in self._specs)) + + def __hash__(self): + return hash(self._specs) + + def __and__(self, other): + if isinstance(other, string_types): + other = SpecifierSet(other) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + specifier = SpecifierSet() + specifier._specs = frozenset(self._specs | other._specs) + + if self._prereleases is None and other._prereleases is not None: + specifier._prereleases = other._prereleases + elif self._prereleases is not None and other._prereleases is None: + specifier._prereleases = self._prereleases + elif self._prereleases == other._prereleases: + specifier._prereleases = self._prereleases + else: + raise ValueError( + "Cannot combine SpecifierSets with True and False prerelease " + "overrides." + ) + + return specifier + + def __eq__(self, other): + if isinstance(other, string_types): + other = SpecifierSet(other) + elif isinstance(other, _IndividualSpecifier): + other = SpecifierSet(str(other)) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + return self._specs == other._specs + + def __ne__(self, other): + if isinstance(other, string_types): + other = SpecifierSet(other) + elif isinstance(other, _IndividualSpecifier): + other = SpecifierSet(str(other)) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + return self._specs != other._specs + + @property + def prereleases(self): + # If we have been given an explicit prerelease modifier, then we'll + # pass that through here. + if self._prereleases is not None: + return self._prereleases + + # Otherwise we'll see if any of the given specifiers accept + # prereleases, if any of them do we'll return True, otherwise False. + # Note: The use of any() here means that an empty set of specifiers + # will always return False, this is an explicit design decision. + return any(s.prereleases for s in self._specs) + + @prereleases.setter + def prereleases(self, value): + self._prereleases = value + + def contains(self, item, prereleases=None): + # Ensure that our item is a Version or LegacyVersion instance. + if not isinstance(item, (LegacyVersion, Version)): + item = parse(item) + + # We can determine if we're going to allow pre-releases by looking to + # see if any of the underlying items supports them. If none of them do + # and this item is a pre-release then we do not allow it and we can + # short circuit that here. + # Note: This means that 1.0.dev1 would not be contained in something + # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 + if (not (self.prereleases or prereleases)) and item.is_prerelease: + return False + + # Determine if we're forcing a prerelease or not, we bypass + # self.prereleases here and use self._prereleases because we want to + # only take into consideration actual *forced* values. The underlying + # specifiers will handle the other logic. + # The logic here is: If prereleases is anything but None, we'll just + # go aheand and continue to use that. However if + # prereleases is None, then we'll use whatever the + # value of self._prereleases is as long as it is not + # None itself. + if prereleases is None and self._prereleases is not None: + prereleases = self._prereleases + + # We simply dispatch to the underlying specs here to make sure that the + # given version is contained within all of them. + # Note: This use of all() here means that an empty set of specifiers + # will always return True, this is an explicit design decision. + return all( + s.contains(item, prereleases=prereleases) + for s in self._specs + ) + + def filter(self, iterable, prereleases=None): + # Determine if we're forcing a prerelease or not, we bypass + # self.prereleases here and use self._prereleases because we want to + # only take into consideration actual *forced* values. The underlying + # specifiers will handle the other logic. + # The logic here is: If prereleases is anything but None, we'll just + # go aheand and continue to use that. However if + # prereleases is None, then we'll use whatever the + # value of self._prereleases is as long as it is not + # None itself. + if prereleases is None and self._prereleases is not None: + prereleases = self._prereleases + + # If we have any specifiers, then we want to wrap our iterable in the + # filter method for each one, this will act as a logical AND amongst + # each specifier. + if self._specs: + for spec in self._specs: + iterable = spec.filter(iterable, prereleases=prereleases) + return iterable + # If we do not have any specifiers, then we need to have a rough filter + # which will filter out any pre-releases, unless there are no final + # releases, and which will filter out LegacyVersion in general. + else: + filtered = [] + found_prereleases = [] + + for item in iterable: + # Ensure that we some kind of Version class for this item. + if not isinstance(item, (LegacyVersion, Version)): + parsed_version = parse(item) + else: + parsed_version = item + + # Filter out any item which is parsed as a LegacyVersion + if isinstance(parsed_version, LegacyVersion): + continue + + # Store any item which is a pre-release for later unless we've + # already found a final version or we are accepting prereleases + if parsed_version.is_prerelease and not prereleases: + if not filtered: + found_prereleases.append(item) + else: + filtered.append(item) + + # If we've found no items except for pre-releases, then we'll go + # ahead and use the pre-releases + if not filtered and found_prereleases and prereleases is None: + return found_prereleases + + return filtered diff --git a/setuptools/_vendor/packaging/version.py b/setuptools/_vendor/packaging/version.py new file mode 100644 index 00000000..e76e9607 --- /dev/null +++ b/setuptools/_vendor/packaging/version.py @@ -0,0 +1,376 @@ +# Copyright 2014 Donald Stufft +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import, division, print_function + +import collections +import itertools +import re + +from ._structures import Infinity + + +__all__ = [ + "parse", "Version", "LegacyVersion", "InvalidVersion", +] + + +_Version = collections.namedtuple( + "_Version", + ["epoch", "release", "dev", "pre", "post", "local"], +) + + +def parse(version): + """ + Parse the given version string and return either a :class:`Version` object + or a :class:`LegacyVersion` object depending on if the given version is + a valid PEP 440 version or a legacy version. + """ + try: + return Version(version) + except InvalidVersion: + return LegacyVersion(version) + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class _BaseVersion(object): + + def __hash__(self): + return hash(self._key) + + def __lt__(self, other): + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + return self._compare(other, lambda s, o: s <= o) + + def __eq__(self, other): + return self._compare(other, lambda s, o: s == o) + + def __ge__(self, other): + return self._compare(other, lambda s, o: s >= o) + + def __gt__(self, other): + return self._compare(other, lambda s, o: s > o) + + def __ne__(self, other): + return self._compare(other, lambda s, o: s != o) + + def _compare(self, other, method): + if not isinstance(other, _BaseVersion): + return NotImplemented + + return method(self._key, other._key) + + +class LegacyVersion(_BaseVersion): + + def __init__(self, version): + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self): + return self._version + + def __repr__(self): + return "<LegacyVersion({0})>".format(repr(str(self))) + + @property + def public(self): + return self._version + + @property + def local(self): + return None + + @property + def is_prerelease(self): + return False + + +_legacy_version_component_re = re.compile( + r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, +) + +_legacy_version_replacement_map = { + "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", +} + + +def _parse_version_parts(s): + for part in _legacy_version_component_re.split(s): + part = _legacy_version_replacement_map.get(part, part) + + if not part or part == ".": + continue + + if part[:1] in "0123456789": + # pad for numeric comparison + yield part.zfill(8) + else: + yield "*" + part + + # ensure that alpha/beta/candidate are before final + yield "*final" + + +def _legacy_cmpkey(version): + # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch + # greater than or equal to 0. This will effectively put the LegacyVersion, + # which uses the defacto standard originally implemented by setuptools, + # as before all PEP 440 versions. + epoch = -1 + + # This scheme is taken from pkg_resources.parse_version setuptools prior to + # it's adoption of the packaging library. + parts = [] + for part in _parse_version_parts(version.lower()): + if part.startswith("*"): + # remove "-" before a prerelease tag + if part < "*final": + while parts and parts[-1] == "*final-": + parts.pop() + + # remove trailing zeros from each series of numeric parts + while parts and parts[-1] == "00000000": + parts.pop() + + parts.append(part) + parts = tuple(parts) + + return epoch, parts + + +class Version(_BaseVersion): + + _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) + + @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 diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt new file mode 100644 index 00000000..b86bba00 --- /dev/null +++ b/setuptools/_vendor/vendored.txt @@ -0,0 +1 @@ +packaging==14.3 diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 06764a17..43df87dc 100755 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -10,6 +10,13 @@ import os import re import sys +try: + import packaging.version +except ImportError: + # fallback to vendored version + import setuptools._vendor.packaging.version + packaging = setuptools._vendor.packaging + from setuptools import Command from setuptools.command.sdist import sdist from setuptools.compat import basestring, PY3, StringIO @@ -68,10 +75,15 @@ class egg_info(Command): self.vtags = self.tags() self.egg_version = self.tagged_version() + parsed_version = parse_version(self.egg_version) + try: + is_version = isinstance(parsed_version, packaging.version.Version) + spec = ( + "%s==%s" if is_version else "%s===%s" + ) list( - parse_requirements('%s==%s' % (self.egg_name, - self.egg_version)) + parse_requirements(spec % (self.egg_name, self.egg_version)) ) except ValueError: raise distutils.errors.DistutilsOptionError( diff --git a/setuptools/dist.py b/setuptools/dist.py index 6b9d350e..e44796fd 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -13,11 +13,19 @@ from distutils.core import Distribution as _Distribution from distutils.errors import (DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError) +try: + import packaging.version +except ImportError: + # fallback to vendored version + import setuptools._vendor.packaging.version + packaging = setuptools._vendor.packaging + from setuptools.depends import Require from setuptools.compat import basestring, PY2 from setuptools import windows_support import pkg_resources + def _get_unpatched(cls): """Protect against re-patching the distutils if reloaded @@ -269,6 +277,27 @@ class Distribution(_Distribution): # Some people apparently take "version number" too literally :) self.metadata.version = str(self.metadata.version) + if self.metadata.version is not None: + try: + ver = packaging.version.Version(self.metadata.version) + normalized_version = str(ver) + if self.metadata.version != normalized_version: + warnings.warn( + "The version specified requires normalization, " + "consider using '%s' instead of '%s'." % ( + normalized_version, + self.metadata.version, + ) + ) + self.metadata.version = normalized_version + except (packaging.version.InvalidVersion, TypeError): + warnings.warn( + "The version specified (%r) is an invalid version, this " + "may not work as expected with newer versions of " + "setuptools, pip, and PyPI. Please see PEP 440 for more " + "details." % self.metadata.version + ) + def parse_command_line(self): """Process features after parsing command line options""" result = _Distribution.parse_command_line(self) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index e803a41e..4c4f9456 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -155,7 +155,7 @@ class TestSvnDummy(environment.ZippedEnvironment): infile.close() del infile - self.assertTrue("Version: 0.1.1-r1\n" in read_contents) + self.assertTrue("Version: 0.1.1.post1\n" in read_contents) @skipIf(not test_svn._svn_check, "No SVN to text, in the first place") def test_no_tags(self): diff --git a/setuptools/tests/test_resources.py b/setuptools/tests/test_resources.py index 3baa3ab1..356e1ed4 100644 --- a/setuptools/tests/test_resources.py +++ b/setuptools/tests/test_resources.py @@ -8,6 +8,10 @@ import tempfile import shutil from unittest import TestCase +import setuptools._vendor.packaging.version +import setuptools._vendor.packaging.specifiers +packaging = setuptools._vendor.packaging + import pkg_resources from pkg_resources import (parse_requirements, VersionConflict, parse_version, Distribution, EntryPoint, Requirement, safe_version, safe_name, @@ -103,7 +107,7 @@ class DistroTests(TestCase): def checkFooPkg(self,d): self.assertEqual(d.project_name, "FooPkg") self.assertEqual(d.key, "foopkg") - self.assertEqual(d.version, "1.3-1") + self.assertEqual(d.version, "1.3.post1") self.assertEqual(d.py_version, "2.4") self.assertEqual(d.platform, "win32") self.assertEqual(d.parsed_version, parse_version("1.3-1")) @@ -120,9 +124,9 @@ class DistroTests(TestCase): self.assertEqual(d.platform, None) def testDistroParse(self): - d = dist_from_fn("FooPkg-1.3_1-py2.4-win32.egg") + d = dist_from_fn("FooPkg-1.3.post1-py2.4-win32.egg") self.checkFooPkg(d) - d = dist_from_fn("FooPkg-1.3_1-py2.4-win32.egg-info") + d = dist_from_fn("FooPkg-1.3.post1-py2.4-win32.egg-info") self.checkFooPkg(d) def testDistroMetadata(self): @@ -330,24 +334,15 @@ class RequirementsTests(TestCase): self.assertTrue(twist11 not in r) self.assertTrue(twist12 in r) - def testAdvancedContains(self): - r, = parse_requirements("Foo>=1.2,<=1.3,==1.9,>2.0,!=2.5,<3.0,==4.5") - for v in ('1.2','1.2.2','1.3','1.9','2.0.1','2.3','2.6','3.0c1','4.5'): - self.assertTrue(v in r, (v,r)) - for v in ('1.2c1','1.3.1','1.5','1.9.1','2.0','2.5','3.0','4.0'): - self.assertTrue(v not in r, (v,r)) - def testOptionsAndHashing(self): r1 = Requirement.parse("Twisted[foo,bar]>=1.2") r2 = Requirement.parse("Twisted[bar,FOO]>=1.2") - r3 = Requirement.parse("Twisted[BAR,FOO]>=1.2.0") self.assertEqual(r1,r2) - self.assertEqual(r1,r3) self.assertEqual(r1.extras, ("foo","bar")) self.assertEqual(r2.extras, ("bar","foo")) # extras are normalized self.assertEqual(hash(r1), hash(r2)) self.assertEqual( - hash(r1), hash(("twisted", ((">=",parse_version("1.2")),), + hash(r1), hash(("twisted", packaging.specifiers.SpecifierSet(">=1.2"), frozenset(["foo","bar"]))) ) @@ -420,7 +415,7 @@ class ParseTests(TestCase): self.assertNotEqual(safe_name("peak.web"), "peak-web") def testSafeVersion(self): - self.assertEqual(safe_version("1.2-1"), "1.2-1") + self.assertEqual(safe_version("1.2-1"), "1.2.post1") self.assertEqual(safe_version("1.2 alpha"), "1.2.alpha") self.assertEqual(safe_version("2.3.4 20050521"), "2.3.4.20050521") self.assertEqual(safe_version("Money$$$Maker"), "Money-Maker") @@ -454,12 +449,12 @@ class ParseTests(TestCase): c('0.4', '0.4.0') c('0.4.0.0', '0.4.0') c('0.4.0-0', '0.4-0') - c('0pl1', '0.0pl1') + c('0post1', '0.0post1') c('0pre1', '0.0c1') c('0.0.0preview1', '0c1') c('0.0c1', '0-rc1') c('1.2a1', '1.2.a.1') - c('1.2...a', '1.2a') + c('1.2.a', '1.2a') def testVersionOrdering(self): def c(s1,s2): @@ -472,16 +467,14 @@ class ParseTests(TestCase): c('2.3a1', '2.3') c('2.1-1', '2.1-2') c('2.1-1', '2.1.1') - c('2.1', '2.1pl4') + c('2.1', '2.1post4') c('2.1a0-20040501', '2.1') c('1.1', '02.1') - c('A56','B27') - c('3.2', '3.2.pl0') - c('3.2-1', '3.2pl1') - c('3.2pl1', '3.2pl1-1') + c('3.2', '3.2.post0') + c('3.2post1', '3.2post2') c('0.4', '4.0') c('0.0.4', '0.4.0') - c('0pl1', '0.4pl1') + c('0post1', '0.4post1') c('2.1.0-rc1','2.1.0') c('2.1dev','2.1a0') diff --git a/setuptools/version.py b/setuptools/version.py index 0ab618ea..4ca9d5df 100644 --- a/setuptools/version.py +++ b/setuptools/version.py @@ -1 +1 @@ -__version__ = '7.1' +__version__ = '8.0' |