summaryrefslogtreecommitdiff
path: root/pkg_resources.py
diff options
context:
space:
mode:
authorDonald Stufft <donald@stufft.io>2014-09-04 21:04:06 -0400
committerDonald Stufft <donald@stufft.io>2014-09-25 20:55:27 -0400
commit9382fa0c05e533400613e1c7c0a777cabb463390 (patch)
tree7cd6f6aebf57ea3b4760cc359b4d4c266a0e04f0 /pkg_resources.py
parent84c9006110e53c84296a05741edb7b9edd305f12 (diff)
downloadpython-setuptools-git-9382fa0c05e533400613e1c7c0a777cabb463390.tar.gz
Implement PEP 440 by using the packaging library
Diffstat (limited to 'pkg_resources.py')
-rw-r--r--pkg_resources.py161
1 files changed, 42 insertions, 119 deletions
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"""