Skip to content

Commit 9382fa0

Browse files
committed
Implement PEP 440 by using the packaging library
1 parent 84c9006 commit 9382fa0

File tree

8 files changed

+105
-195
lines changed

8 files changed

+105
-195
lines changed

CHANGES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
CHANGES
33
=======
44

5-
---
65
6.0
76
---
87

@@ -24,6 +23,9 @@ CHANGES
2423
case-insensitively, but not case-sensitively, should rename those files in
2524
their repository for better portability.
2625

26+
* Implement PEP 440 within pkg_resources and setuptools. This will cause some
27+
versions to no longer be installable without using the ``===`` escape hatch.
28+
2729
---
2830
5.8
2931
---

docs/pkg_resources.txt

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ Requirements Parsing
594594

595595
requirement ::= project_name versionspec? extras?
596596
versionspec ::= comparison version (',' comparison version)*
597-
comparison ::= '<' | '<=' | '!=' | '==' | '>=' | '>'
597+
comparison ::= '<' | '<=' | '!=' | '==' | '>=' | '>' | '~=' | '==='
598598
extras ::= '[' extralist? ']'
599599
extralist ::= identifier (',' identifier)*
600600
project_name ::= identifier
@@ -646,13 +646,10 @@ Requirements Parsing
646646
The ``Requirement`` object's version specifiers (``.specs``) are internally
647647
sorted into ascending version order, and used to establish what ranges of
648648
versions are acceptable. Adjacent redundant conditions are effectively
649-
consolidated (e.g. ``">1, >2"`` produces the same results as ``">1"``, and
650-
``"<2,<3"`` produces the same results as``"<3"``). ``"!="`` versions are
649+
consolidated (e.g. ``">1, >2"`` produces the same results as ``">2"``, and
650+
``"<2,<3"`` produces the same results as``"<2"``). ``"!="`` versions are
651651
excised from the ranges they fall within. The version being tested for
652652
acceptability is then checked for membership in the resulting ranges.
653-
(Note that providing conflicting conditions for the same version (e.g.
654-
``"<2,>=2"`` or ``"==2,!=2"``) is meaningless and may therefore produce
655-
bizarre results when compared with actual version number(s).)
656653

657654
``__eq__(other_requirement)``
658655
A requirement compares equal to another requirement if they have
@@ -681,10 +678,7 @@ Requirements Parsing
681678
``specs``
682679
A list of ``(op,version)`` tuples, sorted in ascending parsed-version
683680
order. The `op` in each tuple is a comparison operator, represented as
684-
a string. The `version` is the (unparsed) version number. The relative
685-
order of tuples containing the same version numbers is undefined, since
686-
having more than one operator for a given version is either redundant or
687-
self-contradictory.
681+
a string. The `version` is the (unparsed) version number.
688682

689683

690684
Entry Points
@@ -967,7 +961,7 @@ version
967961
``ValueError`` is raised.
968962

969963
parsed_version
970-
The ``parsed_version`` is a tuple representing a "parsed" form of the
964+
The ``parsed_version`` is an object representing a "parsed" form of the
971965
distribution's ``version``. ``dist.parsed_version`` is a shortcut for
972966
calling ``parse_version(dist.version)``. It is used to compare or sort
973967
distributions by version. (See the `Parsing Utilities`_ section below for
@@ -1541,40 +1535,12 @@ Parsing Utilities
15411535
-----------------
15421536

15431537
``parse_version(version)``
1544-
Parse a project's version string, returning a value that can be used to
1545-
compare versions by chronological order. Semantically, the format is a
1546-
rough cross between distutils' ``StrictVersion`` and ``LooseVersion``
1547-
classes; if you give it versions that would work with ``StrictVersion``,
1548-
then they will compare the same way. Otherwise, comparisons are more like
1549-
a "smarter" form of ``LooseVersion``. It is *possible* to create
1550-
pathological version coding schemes that will fool this parser, but they
1551-
should be very rare in practice.
1552-
1553-
The returned value will be a tuple of strings. Numeric portions of the
1554-
version are padded to 8 digits so they will compare numerically, but
1555-
without relying on how numbers compare relative to strings. Dots are
1556-
dropped, but dashes are retained. Trailing zeros between alpha segments
1557-
or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
1558-
"2.4". Alphanumeric parts are lower-cased.
1559-
1560-
The algorithm assumes that strings like "-" and any alpha string that
1561-
alphabetically follows "final" represents a "patch level". So, "2.4-1"
1562-
is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
1563-
considered newer than "2.4-1", which in turn is newer than "2.4".
1564-
1565-
Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
1566-
come before "final" alphabetically) are assumed to be pre-release versions,
1567-
so that the version "2.4" is considered newer than "2.4a1". Any "-"
1568-
characters preceding a pre-release indicator are removed. (In versions of
1569-
setuptools prior to 0.6a9, "-" characters were not removed, leading to the
1570-
unintuitive result that "0.2-rc1" was considered a newer version than
1571-
"0.2".)
1572-
1573-
Finally, to handle miscellaneous cases, the strings "pre", "preview", and
1574-
"rc" are treated as if they were "c", i.e. as though they were release
1575-
candidates, and therefore are not as new as a version string that does not
1576-
contain them. And the string "dev" is treated as if it were an "@" sign;
1577-
that is, a version coming before even "a" or "alpha".
1538+
Parsed a project's version string as defined by PEP 440. The returned
1539+
value will be an object that represents the version. These objects may
1540+
be compared to each other and sorted. The sorting algorithm is as defined
1541+
by PEP 440 with the addition that any version which is not a valid PEP 440
1542+
version will be considered less than any valid PEP 440 version and the
1543+
invalid versions will continue sorting using the original algorithm.
15781544

15791545
.. _yield_lines():
15801546

@@ -1629,10 +1595,12 @@ Parsing Utilities
16291595
See ``to_filename()``.
16301596

16311597
``safe_version(version)``
1632-
Similar to ``safe_name()`` except that spaces in the input become dots, and
1633-
dots are allowed to exist in the output. As with ``safe_name()``, if you
1634-
are generating a filename from this you should replace any "-" characters
1635-
in the output with underscores.
1598+
This will return the normalized form of any PEP 440 version, if the version
1599+
string is not PEP 440 compatible than it is similar to ``safe_name()``
1600+
except that spaces in the input become dots, and dots are allowed to exist
1601+
in the output. As with ``safe_name()``, if you are generating a filename
1602+
from this you should replace any "-" characters in the output with
1603+
underscores.
16361604

16371605
``safe_extra(extra)``
16381606
Return a "safe" form of an extra's name, suitable for use in a requirement

pkg_resources.py

Lines changed: 42 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@
7373
except ImportError:
7474
pass
7575

76+
# Import packaging.version.parse as parse_version for a compat shim with the
77+
# old parse_version that used to be defined in this file.
78+
from setuptools._vendor.packaging.version import parse as parse_version
79+
80+
from setuptools._vendor.packaging.version import (
81+
Version, InvalidVersion, Specifier,
82+
)
83+
7684

7785
_state_vars = {}
7886

@@ -1143,13 +1151,14 @@ def safe_name(name):
11431151

11441152

11451153
def safe_version(version):
1146-
"""Convert an arbitrary string to a standard version string
1147-
1148-
Spaces become dots, and all other non-alphanumeric characters become
1149-
dashes, with runs of multiple dashes condensed to a single dash.
11501154
"""
1151-
version = version.replace(' ','.')
1152-
return re.sub('[^A-Za-z0-9.]+', '-', version)
1155+
Convert an arbitrary string to a standard version string
1156+
"""
1157+
try:
1158+
return str(Version(version)) # this will normalize the version
1159+
except InvalidVersion:
1160+
version = version.replace(' ','.')
1161+
return re.sub('[^A-Za-z0-9.]+', '-', version)
11531162

11541163

11551164
def safe_extra(extra):
@@ -2067,7 +2076,7 @@ def yield_lines(strs):
20672076
# Distribution or extra
20682077
DISTRO = re.compile(r"\s*((\w|[-.])+)").match
20692078
# ver. info
2070-
VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|[-.])+)").match
2079+
VERSION = re.compile(r"\s*(<=?|>=?|===?|!=|~=)\s*((\w|[-.*_!+])+)").match
20712080
# comma between items
20722081
COMMA = re.compile(r"\s*,").match
20732082
OBRACKET = re.compile(r"\s*\[").match
@@ -2079,67 +2088,6 @@ def yield_lines(strs):
20792088
re.VERBOSE | re.IGNORECASE
20802089
).match
20812090

2082-
component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE)
2083-
replace = {'pre':'c', 'preview':'c','-':'final-','rc':'c','dev':'@'}.get
2084-
2085-
def _parse_version_parts(s):
2086-
for part in component_re.split(s):
2087-
part = replace(part, part)
2088-
if not part or part=='.':
2089-
continue
2090-
if part[:1] in '0123456789':
2091-
# pad for numeric comparison
2092-
yield part.zfill(8)
2093-
else:
2094-
yield '*'+part
2095-
2096-
# ensure that alpha/beta/candidate are before final
2097-
yield '*final'
2098-
2099-
def parse_version(s):
2100-
"""Convert a version string to a chronologically-sortable key
2101-
2102-
This is a rough cross between distutils' StrictVersion and LooseVersion;
2103-
if you give it versions that would work with StrictVersion, then it behaves
2104-
the same; otherwise it acts like a slightly-smarter LooseVersion. It is
2105-
*possible* to create pathological version coding schemes that will fool
2106-
this parser, but they should be very rare in practice.
2107-
2108-
The returned value will be a tuple of strings. Numeric portions of the
2109-
version are padded to 8 digits so they will compare numerically, but
2110-
without relying on how numbers compare relative to strings. Dots are
2111-
dropped, but dashes are retained. Trailing zeros between alpha segments
2112-
or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
2113-
"2.4". Alphanumeric parts are lower-cased.
2114-
2115-
The algorithm assumes that strings like "-" and any alpha string that
2116-
alphabetically follows "final" represents a "patch level". So, "2.4-1"
2117-
is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
2118-
considered newer than "2.4-1", which in turn is newer than "2.4".
2119-
2120-
Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
2121-
come before "final" alphabetically) are assumed to be pre-release versions,
2122-
so that the version "2.4" is considered newer than "2.4a1".
2123-
2124-
Finally, to handle miscellaneous cases, the strings "pre", "preview", and
2125-
"rc" are treated as if they were "c", i.e. as though they were release
2126-
candidates, and therefore are not as new as a version string that does not
2127-
contain them, and "dev" is replaced with an '@' so that it sorts lower than
2128-
than any other pre-release tag.
2129-
"""
2130-
parts = []
2131-
for part in _parse_version_parts(s.lower()):
2132-
if part.startswith('*'):
2133-
# remove '-' before a prerelease tag
2134-
if part < '*final':
2135-
while parts and parts[-1] == '*final-':
2136-
parts.pop()
2137-
# remove trailing zeros from each series of numeric parts
2138-
while parts and parts[-1]=='00000000':
2139-
parts.pop()
2140-
parts.append(part)
2141-
return tuple(parts)
2142-
21432091

21442092
class EntryPoint(object):
21452093
"""Object representing an advertised importable object"""
@@ -2292,7 +2240,7 @@ def from_location(cls, location, basename, metadata=None,**kw):
22922240
@property
22932241
def hashcmp(self):
22942242
return (
2295-
getattr(self, 'parsed_version', ()),
2243+
self.parsed_version,
22962244
self.precedence,
22972245
self.key,
22982246
_remove_md5_fragment(self.location),
@@ -2338,11 +2286,10 @@ def key(self):
23382286

23392287
@property
23402288
def parsed_version(self):
2341-
try:
2342-
return self._parsed_version
2343-
except AttributeError:
2344-
self._parsed_version = pv = parse_version(self.version)
2345-
return pv
2289+
if not hasattr(self, "_parsed_version"):
2290+
self._parsed_version = parse_version(self.version)
2291+
2292+
return self._parsed_version
23462293

23472294
@property
23482295
def version(self):
@@ -2447,7 +2394,12 @@ def from_filename(cls, filename, metadata=None, **kw):
24472394

24482395
def as_requirement(self):
24492396
"""Return a ``Requirement`` that matches this distribution exactly"""
2450-
return Requirement.parse('%s==%s' % (self.project_name, self.version))
2397+
if isinstance(self.parsed_version, Version):
2398+
spec = "%s==%s" % (self.project_name, self.parsed_version)
2399+
else:
2400+
spec = "%s===%s" % (self.project_name, self.parsed_version)
2401+
2402+
return Requirement.parse(spec)
24512403

24522404
def load_entry_point(self, group, name):
24532405
"""Return the `name` entry point of `group` or raise ImportError"""
@@ -2699,7 +2651,7 @@ def scan_list(ITEM, TERMINATOR, line, p, groups, item_name):
26992651

27002652
line, p, specs = scan_list(VERSION, LINE_END, line, p, (1, 2),
27012653
"version spec")
2702-
specs = [(op, safe_version(val)) for op, val in specs]
2654+
specs = [(op, val) for op, val in specs]
27032655
yield Requirement(project_name, specs, extras)
27042656

27052657

@@ -2708,26 +2660,23 @@ def __init__(self, project_name, specs, extras):
27082660
"""DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!"""
27092661
self.unsafe_name, project_name = project_name, safe_name(project_name)
27102662
self.project_name, self.key = project_name, project_name.lower()
2711-
index = [
2712-
(parse_version(v), state_machine[op], op, v)
2713-
for op, v in specs
2714-
]
2715-
index.sort()
2716-
self.specs = [(op, ver) for parsed, trans, op, ver in index]
2717-
self.index, self.extras = index, tuple(map(safe_extra, extras))
2663+
self.specifier = Specifier(
2664+
",".join(["".join([x, y]) for x, y in specs])
2665+
)
2666+
self.specs = specs
2667+
self.extras = tuple(map(safe_extra, extras))
27182668
self.hashCmp = (
27192669
self.key,
2720-
tuple((op, parsed) for parsed, trans, op, ver in index),
2670+
self.specifier,
27212671
frozenset(self.extras),
27222672
)
27232673
self.__hash = hash(self.hashCmp)
27242674

27252675
def __str__(self):
2726-
specs = ','.join([''.join(s) for s in self.specs])
27272676
extras = ','.join(self.extras)
27282677
if extras:
27292678
extras = '[%s]' % extras
2730-
return '%s%s%s' % (self.project_name, extras, specs)
2679+
return '%s%s%s' % (self.project_name, extras, self.specifier)
27312680

27322681
def __eq__(self, other):
27332682
return (
@@ -2739,29 +2688,13 @@ def __contains__(self, item):
27392688
if isinstance(item, Distribution):
27402689
if item.key != self.key:
27412690
return False
2742-
# only get if we need it
2743-
if self.index:
2744-
item = item.parsed_version
2745-
elif isinstance(item, string_types):
2746-
item = parse_version(item)
2747-
last = None
2748-
# -1, 0, 1
2749-
compare = lambda a, b: (a > b) - (a < b)
2750-
for parsed, trans, op, ver in self.index:
2751-
# Indexing: 0, 1, -1
2752-
action = trans[compare(item, parsed)]
2753-
if action == 'F':
2754-
return False
2755-
elif action == 'T':
2756-
return True
2757-
elif action == '+':
2758-
last = True
2759-
elif action == '-' or last is None:
2760-
last = False
2761-
# no rules encountered
2762-
if last is None:
2763-
last = True
2764-
return last
2691+
2692+
item = item.version
2693+
2694+
# Allow prereleases always in order to match the previous behavior of
2695+
# this method. In the future this should be smarter and follow PEP 440
2696+
# more accurately.
2697+
return self.specifier.contains(item, prereleases=True)
27652698

27662699
def __hash__(self):
27672700
return self.__hash
@@ -2777,16 +2710,6 @@ def parse(s):
27772710
raise ValueError("Expected only one requirement", s)
27782711
raise ValueError("No requirements found", s)
27792712

2780-
state_machine = {
2781-
# =><
2782-
'<': '--T',
2783-
'<=': 'T-T',
2784-
'>': 'F+F',
2785-
'>=': 'T+F',
2786-
'==': 'T..',
2787-
'!=': 'F++',
2788-
}
2789-
27902713

27912714
def _get_mro(cls):
27922715
"""Get an mro for a type or classic class"""

setuptools/_vendor/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)