diff options
| -rw-r--r-- | CHANGES.txt | 4 | ||||
| -rw-r--r-- | docs/pkg_resources.txt | 66 | ||||
| -rw-r--r-- | pkg_resources.py | 161 | ||||
| -rw-r--r-- | setuptools/_vendor/__init__.py | 0 | ||||
| -rwxr-xr-x | setuptools/command/egg_info.py | 8 | ||||
| -rw-r--r-- | setuptools/dist.py | 21 | ||||
| -rw-r--r-- | setuptools/tests/test_egg_info.py | 6 | ||||
| -rw-r--r-- | setuptools/tests/test_resources.py | 34 |
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') |
