summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt4
-rw-r--r--docs/pkg_resources.txt66
-rw-r--r--pkg_resources.py161
-rw-r--r--setuptools/_vendor/__init__.py0
-rwxr-xr-xsetuptools/command/egg_info.py8
-rw-r--r--setuptools/dist.py21
-rw-r--r--setuptools/tests/test_egg_info.py6
-rw-r--r--setuptools/tests/test_resources.py34
8 files changed, 105 insertions, 195 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index a6e27372..e458d76c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -2,7 +2,6 @@
CHANGES
=======
----
6.0
---
@@ -24,6 +23,9 @@ CHANGES
case-insensitively, but not case-sensitively, should rename those files in
their repository for better portability.
+* Implement PEP 440 within pkg_resources and setuptools. This will cause some
+ versions to no longer be installable without using the ``===`` escape hatch.
+
---
5.8
---
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/pkg_resources.py b/pkg_resources.py
index 517298c9..b59ec523 100644
--- a/pkg_resources.py
+++ b/pkg_resources.py
@@ -73,6 +73,14 @@ try:
except ImportError:
pass
+# Import packaging.version.parse as parse_version for a compat shim with the
+# old parse_version that used to be defined in this file.
+from setuptools._vendor.packaging.version import parse as parse_version
+
+from setuptools._vendor.packaging.version import (
+ Version, InvalidVersion, Specifier,
+)
+
_state_vars = {}
@@ -1143,13 +1151,14 @@ 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:
+ return str(Version(version)) # this will normalize the version
+ except InvalidVersion:
+ version = version.replace(' ','.')
+ return re.sub('[^A-Za-z0-9.]+', '-', version)
def safe_extra(extra):
@@ -2067,7 +2076,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
@@ -2079,67 +2088,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"""
@@ -2292,7 +2240,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),
@@ -2338,11 +2286,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):
@@ -2447,7 +2394,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, 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"""
@@ -2699,7 +2651,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)
@@ -2708,26 +2660,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 = Specifier(
+ ",".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 (
@@ -2739,29 +2688,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
@@ -2777,16 +2710,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/command/egg_info.py b/setuptools/command/egg_info.py
index 72493d0b..cb67255b 100755
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -15,6 +15,7 @@ from setuptools.command.sdist import sdist
from setuptools.compat import basestring, PY3, StringIO
from setuptools import svn_utils
from setuptools.command.sdist import walk_revctrl
+from setuptools._vendor.packaging.version import Version
from pkg_resources import (
parse_requirements, safe_name, parse_version,
safe_version, yield_lines, EntryPoint, iter_entry_points, to_filename)
@@ -68,9 +69,14 @@ class egg_info(Command):
self.vtags = self.tags()
self.egg_version = self.tagged_version()
+ parsed_version = parse_version(self.egg_version)
+
try:
+ spec = (
+ "%s==%s" if isinstance(parsed_version, Version) else "%s===%s"
+ )
list(
- parse_requirements('%s==%s' % (self.egg_name,
+ parse_requirements(spec % (self.egg_name,
self.egg_version))
)
except ValueError:
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 8b36f67c..ae4ff554 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -15,6 +15,7 @@ from distutils.errors import (DistutilsOptionError, DistutilsPlatformError,
from setuptools.depends import Require
from setuptools.compat import basestring, PY2
+from setuptools._vendor.packaging.version import Version, InvalidVersion
import pkg_resources
def _get_unpatched(cls):
@@ -268,6 +269,26 @@ 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:
+ normalized_version = str(Version(self.metadata.version))
+ 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 (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 7531e37c..4c4f9456 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -34,7 +34,7 @@ class TestEggInfo(unittest.TestCase):
entries_f = open(fn, 'wb')
entries_f.write(entries)
entries_f.close()
-
+
@skipIf(not test_svn._svn_check, "No SVN to text, in the first place")
def test_version_10_format(self):
"""
@@ -140,7 +140,7 @@ class TestSvnDummy(environment.ZippedEnvironment):
@skipIf(not test_svn._svn_check, "No SVN to text, in the first place")
def test_svn_tags(self):
- code, data = environment.run_setup_py(["egg_info",
+ code, data = environment.run_setup_py(["egg_info",
"--tag-svn-revision"],
pypath=self.old_cwd,
data_stream=1)
@@ -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..9051b414 100644
--- a/setuptools/tests/test_resources.py
+++ b/setuptools/tests/test_resources.py
@@ -16,6 +16,7 @@ from pkg_resources import (parse_requirements, VersionConflict, parse_version,
from setuptools.command.easy_install import (get_script_header, is_sh,
nt_quote_arg)
from setuptools.compat import StringIO, iteritems, PY3
+from setuptools._vendor.packaging.version import Specifier
from .py26compat import skipIf
def safe_repr(obj, short=False):
@@ -103,7 +104,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 +121,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 +331,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", Specifier(">=1.2"),
frozenset(["foo","bar"])))
)
@@ -420,7 +412,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 +446,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 +464,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')